1. Fundamentos

1.1 Instalación

Primero eliminamos cualquier instalación de Go que tengamos en el sistema.

rm -rf /usr/local/go

Lo segundo seria descargar el .tar desde la web oficial

Estando en la misma carpeta que tenemos el .tar desde la terminal hacemos:

tar -C /usr/local -xzf go1.21.0.linux-amd64.tar.gz

Asegúrate de reemplazar go1.21.0.linux-amd64.tar.gz por el que bajaste vos!

Agregamos el path a nuestras variables de entorno, esto permitirá que podamos usar el CLI go en la terminal.

Abre tu archivo de perfil de shell en un editor. Si estás usando el shell Bash, el archivo suele ser ~/.bashrc, y si estás usando Zsh, suele ser ~/.zshrc.

nano ~/.bashrc  # Para Bash
nano ~/.zshrc   # Para Zsh

Dentro pegamos:

export PATH=$PATH:/usr/local/go/bin

Reinicia la terminal y deberías poder poner go version.

Si esto no te resulto, podes intentar descomprimir el tar y pegarlo en la raíz de tu $HOME quedaria asi :

$HOME/go luego en el archivo .zsh o .bashrc pegas esto:

export GOPATH=$HOME/go

1.1 Dependencias utiles

sudo apt-get install gcc-multilib
sudo apt-get install build-essential

1.2 Que es un modulo en Go?

Podemos pensar en un módulo de Go como un nombre que identifica de manera única a tu proyecto entre todos los módulos existentes, ya sean oficiales o de la comunidad.

Esta identificación única es importante para evitar conflictos entre diferentes proyectos y para establecer dependencias claras entre los diferentes componentes de un proyecto.

Por lo general para asegurarme de tener un nombre de modulo único lo que hago es usar la URL de mi repositorio de github de la siguiente manera github.com/nahueldev23/mi-repo-in-go .

Como este articulo lo estoy basando en el libro Let’s Go de Alex Edwars voy a seguir la nomenclatura que propone el la cual es de la siguiente manera: nombre-de-mi-proyecto.tu-nombre-o-nick.com.

Ojota! (advertencia): Si usas la nomenclatura de Github no es correcto usar el prefijo https como seria:

https://github.com/nahueldev23/mi-repo-in-go esto te dará error al intentar crear el modulo que veremos a continuación.

Si tu código va a ser usado por otras personas o programas es buena practica que el nombre del modulo sea el mismo que la URL donde puedan descargarlo, por ejemplo: github.com/tu-repo/el-proyecto

1.3 Hola mundo en Go

Bien como uso Linux voy a dar los pasos básicos para crear una carpeta de manera fantasmal :ghost: , podes hacerlo de la manera que te resulte mas cómoda en tu sistema operativo favorito.

Creamos una carpeta para nuestro proyecto, yo lo voy a hacer dentro de una capeta en Documentos. /home/nahueldev23/Documents/

mkdir snippetbox # Creamos la carpeta
cd snippetbox # Entramos a la carpeta
go mod init snippetbox.nahueldev23.com # convertimos nuestro proyecto en un modulo

Si todo salio bien, veremos que se nos creo un archivo llamado go.mod en nuestro directorio.

Las ventajas que tenemos al hacer que nuestro proyecto sea un module son las siguientes:

  • Hace mas fácil manejar dependencias de terceros
  • Evita supply-chain attacks
  • Otros.

Supplu-chain attacks : Es cuando los atacantes aprovechan los procesos de desarrollo, distribución o actualización de software para insertar malware o vulnerabilidades en el código fuente o en los binarios de un programa. Estos ataques pueden ser muy peligrosos, ya que afectan a los usuarios finales que confían en el software legítimo que están utilizando.

Para continuar creamos el archivo main.go

touch main.go

Dentro de el pondremos:

package main

import "fmt"

func main(){
    fmt.Println("Hola mundo")
}

Ejecutamos en la terminal:

go run .

Deberíamos poder ver en la terminal el mensaje “Hola mundo”

Si no lo ves, asegúrate de que estas parado dentro del proyecto con la terminal.

1.3 Básico - Aplicaciones web

Para crear una aplicación web vamos a necesitar tres cosas esenciales:

  • Un handler Si conoces el patrón MVC podes ver a los handlers como si fueran controladores, los cuales se encargan de ejecutar la lógica de la aplicación y escribir las respuestas http tanto de los body como los headers,
  • Lo segundo es un router o (servemux en Go). Estos se encargan de relacionar los parámetros de las URL con los handlers correspondientes.
  • Lo ultimo es un web server . Lo bueno de Go es que el core del lenguaje ya viene con su propias herramientas para levantar el servicio y recibir peticiones sin la necesidad de usar librerías de terceros,como lo es Nginx o Apache.

En nuestro archivo main.go :

package main
import (
    "log"
    "net/http"
)
// Define a home handler function which writes a byte slice containing
// "Hello from Snippetbox" as the response body.
func home(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hola desde Snippetbox"))
}
func main() {
    
    mux := http.NewServeMux()
    mux.HandleFunc("/", home)
    
    log.Print("Starting server on :4000")
    err := http.ListenAndServe(":4000", mux)
    if err != nil {
        log.Fatal(err)
    }
    
}
func home(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("Hola desde Snippetbox"))
}

Home es una función normal de Go que recibe dos parámetros, http.ResponseWriter nos proporciona métodos para crear una respuesta HTTP y enviarlo al usuario, mientras que *http.Request es un puntero a una estructura que tiene la información de la petición actual.

mux := http.NewServeMux()
mux.HandleFunc("/", home)

Usamos http.NewServerMux() para inicializar un nuevo servemux, luego registramos en la ruta ”/” con la función home.

log.Print("Starting server on :4000")
    err := http.ListenAndServe(":4000", mux)
    if err != nil {
        log.Fatal(err)
    }

Inicializamos el server con http.ListenAndServe le pasamos dos parámetros, el primero la dirección TCP de la red, en este caso al poner :4000 seria localhost:4000 y el segundo es la instancia del servemux.http.ListenAndServe puede retornar un error así que lo manejamos con la condicional if , luego lanzamos log.Fatal(err) si es que entra en la condición.

En Go, la función log.Fatal() se encuentra en el paquete log y se utiliza para imprimir un mensaje de error y luego finalizar el programa con un código de estado de salida no cero. Básicamente, log.Fatal() emite un mensaje de error y luego llama a os.Exit(1) para terminar inmediatamente la ejecución del programa.

En los sistemas operativos tipo Unix (incluidos Linux y macOS), los programas terminan su ejecución devolviendo un valor numérico conocido como “código de estado de salida”. Este código de estado de salida indica el resultado de la ejecución del programa y generalmente se utiliza para indicar si el programa se ejecutó con éxito o si ocurrió algún tipo de error.

Un código de estado de salida igual a 0 generalmente se interpreta como que el programa se ejecutó sin problemas y completó su tarea con éxito. Los códigos de estado diferentes de 0 indican que algo salió mal o que ocurrió algún tipo de error. Los valores específicos de los códigos de estado no 0 pueden tener significados particulares dependiendo del programa y del contexto en el que se utilice.

Corremos:

go run .

Si vamos a nuestro navegador y ponemos en la barra de direcciones localhost:4000 deberíamos poder ver el mensaje Hola desde Snippetbox

Servemux usa la ruta ”/” como un comodín, por lo cual si intentamos navegar a otra ruta como puede ser http://localhost:4000/some obtendremos la respuesta seteda en la función home

Para cerrar el servidor ve a la terminal donde lo iniciaste y presiona Ctrl+c

1.4 Rutas

Vamos a crear unas rutas adicionales a nuestro proyecto

URLHandlerAction
/homeMostrar la home page
/snippet/viewsnippetViewMostrar un snippet especifico
/snippet/createsnippetCreateCrear un nuevo snippet

Abrimos main.go y actualizamos lo siguiente:

package main
import (
    "log"
    "net/http"
)
func home(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Hello from Snippetbox"))
}

func snippetView(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Display a specific snippet..."))
}

func snippetCreate(w http.ResponseWriter, r *http.Request) {
    w.Write([]byte("Create a new snippet..."))
}
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", home)
    mux.HandleFunc("/snippet/view", snippetView)
    mux.HandleFunc("/snippet/create", snippetCreate)
    log.Print("Starting server on :4000")
    err := http.ListenAndServe(":4000", mux)
    log.Fatal(err)
}

1.4 Fixed path y subtree patterns

Servemux soporta dos tipos de patrones en las url: fixed paths y subtree paths.

Fixed paths no terminan con slash (”/”) mientras que subtree paths terminan con slash.

Las dos rutas que creamos anteriormente /snippet/view y /snippet/create son ejemplos de fixed paths.

En servemux el patrón fixed path solo hará match a su respectivo handler cuando una request sea exacta a la URL que definimos.

Por otro lado la ruta ”/” con el handler home es un ejemplo de un subtree path (porque termina con un slash).

Otro ejemplo puede ser algo como “/static/”, en estos casos es como si hubiera un comodín al final “/static/**”

por lo que al intentar acceder a una URL si la parte inicial coincide con el subtree path se activara el handler correspondiente.

Es por eso que cuando solo tenemos la ruta ”/” y ninguna otra, al intentar acceder a cualquier cosa por ejemplo “/some” , la respuesta siempre sera el handler que hayamos puesto en el path ”/**”

1.4 Como restringir el path de la URL raiz?

Como podemos hacer para evitar que la ruta ”/” deje de funcionar como un “catch-all” o comodín?

En esta aplicacion que vamos a hacer queremos que la pagina inicial sea mostrada solo si la ruta es exactamente la que definimos, tal como hacemos con las fixed paths y que de otra manera se le envié al usuario una respuesta 404 page not found.

Por default con servemux no es posible hacer esto, pero podemos crear una condicional en el handler home que nos servirá para obtener el mismo efecto.

func home(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
	}
    
	w.Write([]byte("Hello from Snippetbox"))
}

Si estamos corriendo el servemux en la terminal , lo cerramos con Ctrl+C y lo volvemos a levantar.

go run .

Ahora si vamos a cualquier ruta que no exista, por ejemplo “/some” recibiremos la respuesta 404 page not found.

1.4 DefaultServeMux

En Go tenemos otras maneras de registrar rutas con sus respectivos handlers sin que tengamos que declarar un servemux, por ejemplo:

func main() {
    http.HandleFunc("/", home)
    http.HandleFunc("/snippet/view", snippetView)
    http.HandleFunc("/snippet/create", snippetCreate)
    log.Print("Starting server on :4000")
    err := http.ListenAndServe(":4000", nil)
    log.Fatal(err)
}

Por detrás, esas funciones registran sus rutas con algo llamado DefaultServeMux no tiene nada de especial, es una instancia de servemux como la que habíamos cradodo nosotros anteriormente, con la unica diferencia que es inicializado por default y guardado en una variable global de net/http. Así se ve la linea que la crea y la almacena en el código fuente de Go:

var DefaultServeMux = NewServeMux()

Con este enfoque podemos hacer nuestro código un poco mas corto, pero no se recomienda hacerlo así para aplicaciones que vayan a producción.

El motivo es que DefaultServeMux es una variable global, cualquier paquete puede acceder y registrar una ruta (incluyendo librería de terceros que tu aplicación use, si alguna de esas librerías esta comprometida podría hacer uso de DefaultServeMux para exponer un handler malicioso).

Entonces por motivos de seguridad es buena idea evitar DefaultServeMux y usar en su lugar tu propio scope local para instanciar servemux , como lo hicimos anteriormente.

1.4 Información adicional

  • En Go servemux, los patrones de URL mas largos tienen mas prioridad que los cortos. Por lo que si un servemux tiene multiples paths que coinciden con la petición, siempre se responderá con el handler que tenga la URL mas larga. Lo bueno de esto es que no tenes que preocuparte por el orden en le que creas tus rutas.
  • Las peticiones a las URL son sanitizadas automaticamente.Si una request contiene algún . o .. o slashes ”///” repetidos, el usuario automáticamente sera re direccionado a una url equivalente sin esos errores. Por ejemplo, un usuario hace una peticion a */some/like/..//other, se enviara automáticamente un código de respuesta 301 Permanent Redirect e ira a /some/like.
  • Si tenemos registrada un subtree path y el usuario manda la peticion sin el slash final, automáticamente sera redireccionado al subtree path agregándole el slash alfinal junto a un codigo de respuesta 301 Permanent Redirect. Por ejemplo tenemos /some/ y su petición es a /some sera redireccionado a /some/
1.4 Host name matching

Podemos incluir host names en los paths. Esto puede ser útil cuando queres redireccionar todas las peticiones HTTP a una URL canónica o si tu aplicación esta actuando como backend de muchos otros sitios o servicios. Por ejemplo:

mux := http.NewServeMux()
mux.HandleFunc("mi.web.org/", myHandler)
mux.HandleFunc("tu.sitio.org/", yourHandler)
mux.HandleFunc("/some", someHandler)

Cuando se busca la URL que coincida primero verificara los patrones que usen hostname y si no hay match pasara a revisar los paths que no sean específicos del host.

1.4 Que hay sobre las rutas RESTFul?

Es importante saber que la funcionalidad que provee Go servemux es muy ligera y no admite enrutamiento basado en el método de la solicitud ( como puede ser discernir que path mostrar si el la petición es GET , UPDATE, DELETE,POST), tampoco soporta URLs con variables en ella , (“delete/some/id=3”), ni tampoco admite patrones regexp. En otros post veremos librerias de terceros que nos permitiran hacer todo lo que necesitamos para crear una RESTul API.

1.5 Personalizacion de encabezados HTTP

Vamos a actualizar nuestra aplicacion para que la ruta /snippet/create solo responda a las peticiones HTTP que sean de tipo POST.

MethodPatternHandlerAction
ANY/homeMostrar la home page
ANY/snippet/viewsnippetViewMostrar un snippet especifico
POST/snippet/createsnippetCreateCrear un nuevo snippet

Hacer este cambio en la ruta /snippet/create es importante porque a traves de esta ruta modifcaremos la informacion de nuestra base de datos, entonce debemos seguir las HTTP good practices y restringir esta peticion solo para el verbo POST.

1.4 HTTP status codes

Comencemos actualizando nuestro handler snippetCreate para que responda al usuario un 405 (method not allowed) como HTTP status, al menos que la peticion sea a traves del metodo POST. Para hacerlo necesitaremos usar w.WriteHeader() de la siguiente manera:

func snippetCreate(w http.ResponseWriter, r *http.Request) {
    // Use r.Method to check whether the request is using POST or not.
    if r.Method != "POST" {
        // If it's not, use the w.WriteHeader() method to send a 405 status
        // code and the w.Write() method to write a "Method Not Allowed"
        // response body. We then return from the function so that the
        // subsequent code is not executed.
        w.WriteHeader(405)
        w.Write([]byte("Method Not Allowed"))
        return
    }
    
    w.Write([]byte("Create a new snippet..."))
}

1.5 Detalles con w.WriteHeader

  • Solo es posible llamar a w.WriteHeader() una unica vez por respuesta, una vez que el code status ha sido escrito no podra ser cambiado. Si intentas volver a llamar a w.WriteHeader() una segunda vez Go lanzara un mensaje de advertencia.
  • Si no llamas a w.WriteHeader() explicitamente, entonces en la primera llamada a w.Write() automaticamente enviara un status 200 OK al usuario. Entonces si intentas enviar un status distinto a 200, tenes que llamar a w.WriteHeader() antes que a w.Write().

Reiniciamos el servidor y enviamos a travez de postman , hoppscotch , curl o cualquier otro programa la peticion a http://localhost:4000/snippet/create

$ curl -i -X POST http://localhost:4000/snippet/create
HTTP/1.1 200 OK
Date: Sat, 29 Jan 2022 10:27:14 GMT
Content-Length: 23
Content-Type: text/plain; charset=utf-8
Create a new snippet...

Pero si enviamos otro metodo como GET,PUT o DELETE deberiamos obtener la respuesta 405 Method Not Allowed. Por ejemplo:

$ curl -i -X PUT http://localhost:4000/snippet/create
HTTP/1.1 405 Method Not Allowed
Date: Sat, 29 Jan 2022 10:27:55 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8
Method Not Allowed

1.5 Personalizando headers

Otra mejora que podemos hacer es incluir un Allow header junto con la respuesta 405 Method Not Allowed que le permitira al usuario saber cuales metodos soporta esa URL.

Podemos lograr esto usando w.Header().Set() para agreagar un nuevo header al mapa de encabezados asi:

func snippetCreate(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        // Use the Header().Set() method to add an 'Allow: POST' header to the
        // response header map. The first parameter is the header name, and
        // the second parameter is the header value.
        w.Header().Set("Allow", "POST")
        w.WriteHeader(405)
        w.Write([]byte("Method Not Allowed"))
        return
    }
    w.Write([]byte("Create a new snippet..."))
}

Importante: Cambiar el mapa de encabezados original despues de haber llamado a w.WriteHeader o w.Write no tendra ningun efecto en los encabezados que el usuario reciba.Tenes que asegurarte que el mapa de encabezados contenga todas las respuestas que queres antes de llamar a esos metodos.

Si enviamos una peticion con el nuevo header agregado tenemos que ver algo como esto:

$ curl -i -X PUT http://localhost:4000/snippet/create
HTTP/1.1 405 Method Not Allowed
Allow: POST
Date: Sat, 29 Jan 2022 10:29:12 GMT
Content-Length: 18
Content-Type: text/plain; charset=utf-8
Method Not Allowed

Ahora podemos ver en la respusta: Allow: POST

1.5 http.Error shortcut

Si queremos enviar una respuesta diferente a 200 con un texto plano en la respuesta del body, (como venimos haciendo hasta ahora) es una buena oportunidad para usar http.Error(). Este es un helper function liviano que toma un mensaje y un status code, luego llama a w.WriteHeader y a w.Write detras de escenas por nosotros.

Con este enfoque nuestro codigo se veria de la siguiente manera:

func snippetCreate(w http.ResponseWriter, r *http.Request) {
    if r.Method != "POST" {
        w.Header().Set("Allow", "POST")
        // Use the http.Error() function to send a 405 status code and "Method Not
        // Allowed" string as the response body.
        http.Error(w, "Method Not Allowed", 405)
        return
    }
    w.Write([]byte("Create a new snippet..."))
}

1.5 Constantes net/http

Una cosa mas que podemos hacer es usar las constantes que nos da el paquete net/http en vez de escribirlas nosotros mismos a mano como si de magic string se tratasen.

Podemos usar la constante http.MethodPost en vez de POST y http.StatusMethodNotAllowed en vez de 405, asi:

func snippetCreate(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        w.Header().Set("Allow", http.MethodPost)
        http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
        return	
    }
    
    w.Write([]byte("Create a new snippet..."))
}

Usar estas constantes es una buena practica, ya que nos ayuda a prevenir runtime errors por problemas en los tipos. En el caso de los codigos de status nos ayuda a mantener un codigo mas claro y auto documentado, especialmente cuando usamos status codes menos frecuentes.

Podes encontrar la lista de constantes del paquete net/http aca

1.5 Información adicional

1.5 Encabezados generados por el sistema y rastreo de contenido

Cuando enviamos una respuesta Go automaticamente setea tres headers por nosotros: Date,Content-Length y Content-Type

El header Content-Type intentara setearse correctamente rastreando la respuesta que envies en el body con la funcion http.DetectContentType() . Si esta funcion no puede adivinarla Go usara como fallback : Content-Type: application/octet-stream.

http.DetectContentType() generalmente funciona bien, pero un problema comun para los desarrolladores web que son nuevos en Go es que no puede distinguir entre JSON y texto plano. Entonces por default las respuestas en formato JSON seran enviadas como : Content-Type: text/plain; charset=utf-8 en el header. Podemos prevenir esto seteando manualmente el header asi:

w.Header().Set("Content-Type", "application/json")
w.Write([]byte(`{"name":"Nahuel"}`))
1.5 Manipular el map del header

Anteriormente usamos w.Header().Set() para agregar un nuevo header al map de headers. Pero también tenemos Add() , Del(), Get() y Values() que podemos usar para manipular el map de headers.

// Set a new cache-control header. If an existing "Cache-Control" header exists
// it will be overwritten.
w.Header().Set("Cache-Control", "public, max-age=31536000")
// In contrast, the Add() method appends a new "Cache-Control" header and can
// be called multiple times.
w.Header().Add("Cache-Control", "public")
w.Header().Add("Cache-Control", "max-age=31536000")
// Delete all values for the "Cache-Control" header.
w.Header().Del("Cache-Control")
// Retrieve the first value for the "Cache-Control" header.
w.Header().Get("Cache-Control")
// Retrieve a slice of all values for the "Cache-Control" header.
w.Header().Values("Cache-Control")
1.5 Header canonicalizado

Cuando usamos Add() , Del(), Get() y Values() los nombres de los headers siempre serán convertidos por la función textproto.CanonicalMIMEHeaderKey() . Esta convierte la primera letra y cualquier letra después de un guion en mayúsculas, el resto sera pasado a minúsculas. Esto hace que los encabezados sean insensibles a las mayúsculas y minúsculas, lo que significa que no importa si escribes “Content-Type”, “content-type” o incluso “cOnTeNt-TyPe”, todos serán tratados como el mismo encabezado.

Si queremos evitar este comportamiento por default tenemos que hacer lo siguiente:

w.Header()["X-XSS-Protection"] = []string{"1; mode=block"}

De esta manera evitamos que automáticamente el header se convierta en esto : “X-Xss-Protection”.

Nota: Si se utiliza una conexión HTTP/2, Go siempre convertirá automáticamente el nombres y valores de encabezado en minúsculas según las especificaciones HTTP/2.

1.5 Eliminando headers generados por el sistema

El metodo Del no puede remover headers generados por el sistema, para suprimirlos tenes que acceder al header map directamente y setear el valor a nil.

w.Header()["Date"] = nil

1.6 URL query string

Vamos a modificar nuestro handler snippetView para que acepte el parámetro id que nos enviara el usuario:

MethodPatternHandlerAction
ANY/homeMostra la home page
ANY/snippet/view?id=1snippetViewMostrar snippet especifico
POST/snippet/createsnippetCreateCrear un nuevo snippet

Mas adelante usaremos ese id para obtener el snippet especifico de la base de datos y mostrárselo al usuario, pero por ahora solo leeremos el valor del id que nos pasan por parámetro.

Para lograr esta tarea tenemos que modificar un poco el handler snippetView para que haga dos cosas:

  1. Necesita recibir el valor del id que viene por parámetro a través de al URL, el cual podemos obtenerlo con el metodo r.URL.Query().Get(). Este siempre retornara un valor de tipo string,si el parámetro no existe retornara un string vacio "" .
  2. Como el valor del id no es de confianza, ya que es enviado por el usuario, deberemos validarlo, Para nuestra aplicación necesitamos validar que el valor sea un numero positivo. Podemos hacer esto convirtiendo el valor de tipo string a integer con strconv.Atoi() y chequear posteriormente que el valor es mayor a cero.
package main

import (
    "fmt" // New import
    "log"
    "net/http"
    "strconv" // New import
)

func snippetView(w http.ResponseWriter, r *http.Request) {
    // Extract the value of the id parameter from the query string and try to
    // convert it to an integer using the strconv.Atoi() function. If it can't
    // be converted to an integer, or the value is less than 1, we return a 404 page
    // not found response.
    id, err := strconv.Atoi(r.URL.Query().Get("id"))
    if err != nil || id < 1 {
    	http.NotFound(w, r)
    	return
    }
    // Use the fmt.Fprintf() function to interpolate the id value with our response
    // and write it to the http.ResponseWriter.
    fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
}

Si vamos a localhost deberíamos ver el mensaje especifico.

1.6 io.writer interface

Otra cosa es que si vemos la firma de fmt.Fprintf() vemos que toma como primer parámetro el tipo io.Writter

func Fprintf(w io.Writer, format string, a ...any) (n int, err error)

Pero nosotros le pasamos w que es de tipo http.ResponseWritter y funciona correctamente. Esto es posible porque io.Writter es una interfaz y http.ResponseWritter satisface esa interfaz porque tiene un metodo w.Write(). Por lo que, siempre que veamos en una firma que un parametro espera q io.Writter podemos pasar tranquilamente algo de tipo http.ResponseWritter.

1.7 Estructura de proyecto y organización

Antes de seguir organicemos un poco las cosas. Antes que nada te comento que las practicas que veremos a continuación están inspiradas en esta y este otro repositorio, para las buenas practicas de una estructura en Go. Si bien no hay una sola manera correcta de hacer las cosas, esta estructura nos ayudara en el proceso.

Si venís siguiendo los post anteriores y estas haciendo el proyecto , abrí una terminal en la carpeta raiz y hace lo siguiente:

rm main.go
mkdir -p cmd/web internal ui/html ui/static
touch cmd/web/main.go
touch cmd/web/handlers.go

1.7 Para que usaremos cada uno de estos directorios?

El directorio cmd contendrá el código especifico de cada aplicación ejecutable que queramos hacer, por ahora solo tenemos una aplicacion ejecutable web en cmd/web.

internal contendrá código auxiliar no especifico de la aplicación que sera usado en el proyecto. Lo usaremos para contener codigo reusable como helpers de validación y modelos de base de datos SQL del proyecto.

El directorio ui contendrá los assets del user interface que usaremos para la aplicación web, el ui/html lo usaremos para contener los templates HTML y el directorio ui/static contendrá los elementos estaticos como el CSS y las imagenes.

1.7 Porque esta estructura?

  1. Nos da una clara separación entre los assets de Go y los que no son de Go. Todo lo que escribamos en Go vivirá exclusivamente dentro de cmd y de internal , dejando libre el root del proyecto para assets que no sean de Go como archivos de UI ,makfiles, module definitions (incluyendo go.mod). Esto nos hará las cosas faciles cuando hagamos el build de nuestra app y la despleguemos en el futuro.

  2. Escala muy bien si luego queres crear otra app ejecutable para tu proyecto. Por ejemplo, podrias querer crear un CLI (command line interface) para automatizar algunas tareas adminsitrativas en el futuro. Con esta estructura solo tendrías que crear la aplicación dentro de cmd/cli y tendrás acceso a las importaciones de todo tu proyecto para así reutilizar el código que tengas en internal.

1.7 Refactorizando nuestra app

Vamos a volver a poner lo que borramos , pero esta vez , cada cosa en su lugar:

FILE: cmd/web/main.go

package main
import (
    "log"
    "net/http"
)
func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", home)
    mux.HandleFunc("/snippet/view", snippetView)
    mux.HandleFunc("/snippet/create", snippetCreate)
    log.Print("Starting server on :4000")
    err := http.ListenAndServe(":4000", mux)
    log.Fatal(err)
}
package main
import (
    "fmt"
    "net/http"
    "strconv"
)
func home(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
    http.NotFound(w, r)
    return
}
	w.Write([]byte("Hello from Snippetbox"))
}

func snippetView(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.URL.Query().Get("id"))
    if err != nil || id < 1 {
    http.NotFound(w, r)
    return
}
	fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
}

func snippetCreate(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
    w.Header().Set("Allow", http.MethodPost)
    http.Error(w, "Method Not Allowed", http.StatusMethodNotAllowed)
    return
}
	w.Write([]byte("Create a new snippet..."))
}

Ahora corremos el servidor así:

$ cd $HOME/code/snippetbox
$ go run ./cmd/web
2022/01/29 12:02:26 Starting server on :4000

1.7 Información adicional

1.7 El directorio internal

En Go el directorio internal cumple un rol especial, Es como si se tratara de una plabra clave que le dice a Go que todo el código que pongamos ahí dentro no puede ser usado por otras personas por fuera de nuestro proyecto pricipal, en este caso snippetbox .

Esta regla es útil porque evita que otros proyectos usen y dependan de las cosas que tenemos en nuestra carpeta “internal” (que podrían no estar bien organizadas o no tener soporte) incluso si nuestro proyecto está públicamente disponible en sitios como GitHub.

1.8 HTML template y herencia

Crearemos un template en ui/html/pages/home.html el cual contiene nuestra home page escrito en HTML.

$ cd $HOME/code/snippetbox
$ mkdir ui/html/pages
$ touch ui/html/pages/home.html
<!doctype html>
<html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>Home - Snippetbox</title>
    </head>
    <body>
      <header>
        <h1><a href='/'>Snippetbox</a></h1>
      </header>
      <main>
        <h2>Latest Snippets</h2>
        <p>There's nothing to see here yet!</p>
      </main>
      <footer>Powered by <a href='https://golang.org/'>Go</a></footer>
    </body>
</html>

Ahora la pregunta seria: Como hacemos que nuestro handler home lo renderice?

Para lograrlo tenemos que usar el paquete de Go html/template el cual provee un conjunto de funciones seguras para parsear y renderizar los templates HTML.

Vayamos a cmd/web/handlers.go y agreguemos el siguiente código:

package main
import (
    "fmt"
+    "html/template"
+    "log"
    "net/http"
    "strconv"
)
func home(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }
-    w.Write([]byte("Hello from Snippetbox"))
    // Use the template.ParseFiles() function to read the template file into a
    // template set. If there's an error, we log the detailed error message and use
    // the http.Error() function to send a generic 500 Internal Server Error
    // response to the user.
    ts, err := template.ParseFiles("./ui/html/pages/home.html")
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", 500)
        return
    }
    // We then use the Execute() method on the template set to write the
    // template content as the response body. The last parameter to Execute()
    // represents any dynamic data that we want to pass in, which for now we'll
    // leave as nil.
    err = ts.Execute(w, nil)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", 500)
    }
}

Ahora corremos el servidor:

$ cd $HOME/code/snippetbox
$ go run ./cmd/web
2022/01/29 12:06:02 Starting server on :4000

Si vamos al navegador vamos a poder ver el contenido del HTML.

1.8 Template composition

Como vamos a tener código HTML que vamos querer tener en todos los archivos,como puede ser el header , el navbar o el footer y no queremos tener código duplicado es buena idea tener un template master que contenta los componentes compartidos entre paginas.

Creemos un nuevo archivo de template:

touch iu/html/base.html

Agregamos el siguiente codigo al template:

+{{define "base"}}
 <!doctype html>
 <html lang='en'>
   <head>
        <meta charset='utf-8'>
-       <title>Home - Snippetbox</title>
+       <title>{{template "title" .}} - Snippetbox</title>
    </head>
    <body>
    <header>
    	<h1><a href='/'>Snippetbox</a></h1>
    </header>
    <main>
-    <h2>Latest Snippets</h2>
-    <p>There's nothing to see here yet!</p>
+   	{{template "main" .}}
    </main>
    <footer>Powered by <a href='https://golang.org/'>Go</a></footer>
    </body>
 </html>
 {{end}}

Estamos usando {{define "base"}}...{{end}} para definir un nombre para el template llamado base, el cual contiene los componentes que queremos que tengan en común todas nuestras demás paginas.

Dentro de esto también usamos {{template "title" .}} y {{template "main" .}} para denotar que queremos invocar otros templates (llamados title y main) en un punto en particular del HTML

Nota: Si te lo estás preguntando, que es el punto al final en {{template "title".}} representa cualquier dato dinámico que desees pasar a la plantilla invocada. Hablaremos Más sobre en otros post.

Volvemos a ui/html/pages/home.html y lo dejamos de la siguiente manera:

 {{define "title"}}Home{{end}}
 {{define "main"}}
   <h2>Latest Snippets</h2>
   <p>There's nothing to see here yet!</p>
 {{end}}

Una vez lo hayas actualizado tenemos que cambiar un poco el handler home cmd/web/handlers.go

func home(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }
-   // Use the template.ParseFiles() function to read the template file into a
-   // template set. If there's an error, we log the detailed error message and use
-   // the http.Error() function to send a generic 500 Internal Server Error
-   // response to the user.
-   ts, err := template.ParseFiles("./ui/html/pages/home.html")

+    // Initialize a slice containing the paths to the two files. It's important
+    // to note that the file containing our base template must be the *first*
+    // file in the slice.
+    files := []string{
+    "./ui/html/base.html",
+    "./ui/html/pages/home.html",
+    }

+    // Use the template.ParseFiles() function to read the files and store the
+    // templates in a template set. Notice that we can pass the slice of file
+    // paths as a variadic parameter?
+    ts, err := template.ParseFiles(files...)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", 500)
        return
    }
-   // We then use the Execute() method on the template set to write the
-   // template content as the response body. The last parameter to Execute()
-   // represents any dynamic data that we want to pass in, which for now we'll
-   // leave as nil.
-   err = ts.Execute(w, nil)

+    // Use the ExecuteTemplate() method to write the content of the "base"
+    // template as the response body.
+    err = ts.ExecuteTemplate(w, "base", nil)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", 500)
    }
}

De esta manera en vez de contener el HTML directamente, template.ParseFiles contiene 3 nombres de templates, base,title y main. Luego usamos ExecuteTemplate() para decirle a Go que nosotros queremos específicamente responder usando el contenido de base template ( el cual invoca los template_title_ y main).

Reinicia el servidor y entra a localhost:4000

1.8 Partials

En algunas apps vamos a querer dividir nuestro código en otros mas pequeños llamados partials los cuales podremos usar en distintas partes de nuestra aplicación o layouts. Para ejemplificar vamos a crear un partial que contenga la navegación principal.

Creamos dentro de ui/html/partials/nav.html

$ mkdir ui/html/partials
$ touch ui/html/partials/nav.html
{{define "nav"}}
    <nav>
    	<a href='/'>Home</a>
    </nav>
{{end}}

Actualizamos el template base para que haga uso del template nav.

{{define "base"}}
 <!doctype html>
 <html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>{{template "title" .}} - Snippetbox</title>
    </head>
    <body>
        <header>
        	<h1><a href='/'>Snippetbox</a></h1>
        </header>
+        	<!-- Invoke the navigation template -->
+        	{{template "nav" .}}
        <main>
        	{{template "main" .}}
        </main>
        <footer>Powered by <a href='https://golang.org/'>Go</a></footer>
    </body>
 </html>
 {{end}}

También lo agregamos al handler home:

package main
...
func home(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }
    // Include the navigation partial in the template files.
    files := []string{
    "./ui/html/base.tmpl",
+    "./ui/html/partials/nav.tmpl",
    "./ui/html/pages/home.tmpl",
    }
    ts, err := template.ParseFiles(files...)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", 500)
        return
    }
    err = ts.ExecuteTemplate(w, "base", nil)
    if err != nil {
        log.Print(err.Error())
        http.Error(w, "Internal Server Error", 500)
    }
}
...

Reiniciamos el servidor y deberíamos ver el nav

1.8 Información adicional

1.8 La acción block

En el codigo anterior usamos {{template}} para invocar un template desde otro. Pero Go nos da también {{block}}...{{end}} el cual podemos usar en su lugar. Este funciona de al misma manera que lo hace {{template}} solo que nos permite mostrar un contenido por default si el template invocado no existe.

{{define "base"}}
    <h1>An example template</h1>
    {{block "sidebar" .}}
    	<p>My default sidebar content</p>
    {{end}}
{{end}}	

Pero si queres no tenes porque poner ese valor por defecto dentro de block, simplemente no se mostrara nada si es que no tenemos un template.

Vamos a mejorar la apariencia de neustra pagina con un poco de css , imagenes y un poco

de javascript para resaltar la navegacion activa.

Vamos a descargar los assets y lo descomprimiremos en ./ui/static/

```bash[class=“line-numbers”]

curl https://www.alexedwards.net/static/sb-v2.tar.gz | tar -xvz -C ./ui/static/

```

1.9 http.Fileserver handler

El paquete de Go net/http tiene un handler http.Fileserver el cual podemos usar para servir

archivos de un directorio especifico mediante HTTP. Vamos a agregar una ruta en nuestra aplicacion

para que todas las peticiones que comiencen con /static/ sean manejadas asi:

MethodPatternHandlerAction
ANY/homeDisplay the home page
ANY/snippet/view?id=1snippetViewDisplay a specific snippet
POST/snippet/createsnippetCreateCreate a new snippet
ANY/static/http.FileServerServe a specific static file

Recuerda que /static/ termina con slash por lo que es un subtree path , por lo que actuara como comodin para todas las peticiones que comiencen con /static/

Para hacer uso del handler tenemos que hacerlo de la siguiente manera:

fileServer := http.FileServer(http.Dir("./ui/static/"))

Cuando el handler reciba la peticion, si no eliminamos /static antes de que llegue al FileServer este intentara buscar los recursos en .ui/static/static/image.jpg lo cual es incorrecto. Para que esto funcione correctamente, debemos quitar /static del path de la URL antes de pasarlo por http.FileServer. De otra manera buscara el archivo en un lugar donde no existe y el usuario recibirá un error 404 page not found , Go incluye una funcion para ayudarnos con esta tarea y es http.StripPrefix().

Vamos al archivo main.go y agreguemos el siguiente código.

package main
import (
"log"
"net/http"
)
func main() {
    mux := http.NewServeMux()
    // Create a file server which serves files out of the "./ui/static" directory.
    // Note that the path given to the http.Dir function is relative to the project
    // directory root.
    fileServer := http.FileServer(http.Dir("./ui/static/"))

    // Use the mux.Handle() function to register the file server as the handler for
    // all URL paths that start with "/static/". For matching paths, we strip the
    // "/static" prefix before the request reaches the file server.
    // ex of request: /static/image.jpg , after strip we get /image.jpg.
    mux.Handle("/static/", http.StripPrefix("/static", fileServer))

    // Register the other application routes as normal.
    mux.HandleFunc("/", home)
    mux.HandleFunc("/snippet/view", snippetView)
    mux.HandleFunc("/snippet/create", snippetCreate)
    log.Print("Starting server on :4000")
    err := http.ListenAndServe(":4000", mux)
    log.Fatal(err)
}

Si levantamos el servidor y vamos a localhost:4000 deberiamos ver listadas las carpetas css/ img/ y js/

1.9 Usando los archivos estaticos

Vamos a ui/html/base.html para hacer uso de los archivos estáticos.

 {{define "base"}}
 <!doctype html>
 <html lang='en'>
    <head>
        <meta charset='utf-8'>
        <title>{{template "title" .}} - Snippetbox</title>
+        <!-- Link to the CSS stylesheet and favicon -->
+        <link rel='stylesheet' href='/static/css/main.css'>
+        <link rel='shortcut icon' href='/static/img/favicon.ico' type='image/x-icon'>
+        <!-- Also link to some fonts hosted by Google -->
        <link rel='stylesheet' href='https://fonts.googleapis.com/css?		family=Ubuntu+Mono:400,700'>
    </head>
    <body>
        <header>
        <h1><a href='/'>Snippetbox</a></h1>
        </header>
        {{template "nav" .}}
        <main>
        {{template "main" .}}
        </main>
        <footer>Powered by <a href='https://golang.org/'>Go</a></footer>
+        <!-- And include the JavaScript file -->
+        <script src="/static/js/main.js" type="text/javascript"></script>
    </body>
 </html>
 {{end}}

1.9 Informacion adicional

1.9 Caracteristicas y funciones

FileServer tiene unas caracteristicas que valen la pena nombrar:

  • Sanitiza todas las solicitudes pasando todas ellas a traves de path.Clean() antes de buscar los archivos.En este proceso remueve . y .. de las URL lo cual ayuda a evitar ataques transversales de directorio (directory traversal attacks).Es particularmente util cuando estas usando el fileserver en conjunto con una ruta que no sanitiza las URL automaticamente.

  • Range request son admitidas. Es muy bueno si tu aplicacion tiene archivos muy grandes y queres soportar descargas que se puedan reanudar. Podes ver esta funcionalidad usando curl con peticiones en bytes de 100-199, asi:

$ curl -i -H "Range: bytes=100-199" --output - http://localhost:4000/static/img/logo.png
HTTP/1.1 206 Partial Content
Accept-Ranges: bytes
Content-Length: 100
Content-Range: bytes 100-199/1075
Content-Type: image/png
Last-Modified: Thu, 04 May 2017 13:07:52 GMT
Date: Sat, 29 Jan 2022 14:33:59 GMT
[binary data]
  • Los encabezados Last-Modified y If-Modified-Since son soportados.Si un archivo no ha cambiado desde que el usuario hizo la ultima solicitud http.FileServer enviara el codigo de estado 304 Not Modified en vez del archivo en si.Esto ayuda a reducir la latencia y la sobrecarga de procesamiento tanto como para el cliente como para el servidor.

  • Content-type es automaticamente seteado a partir de la extension del archivo, esto se logra a traves del uso de mime.TypeByExtension().Podemos agregar nuestros propios tipos y contenido personalizado usando mime.AddExtensionType().

1.9 Performance

En el codigo de arriba configuramos nuestro fileserver para que lea los archivos ubicados en ./ui/static de nuestro disco rigido.

Pero es importante notar que una vez la aplicacion este funcionando http.FileServer no siga leyendo los datos desde nuestro disco. Tanto los sitemas windows como UNIX almacenan en cache los archivos utilizados recientemente en la RAM (al menos los archivos mas recurrentes). Es por eso que http.ServerFile servira los datos desde la RAM en lugar de hacerlo de una manera mas lenta con tu disco rigido.

1.9 Sirviendo un solo archivo

Aveces queremos entregar un solo archivo desde nuestro handler. Para eso usamos http.ServeFile:

func downloadHandler(w http.ResponseWriter, r *http.Request) {
  http.ServeFile(w, r, "./ui/static/file.zip")
}

http.ServeFile() no sanitiza automaticamente la ruta del archivo.Para evitar ataques de cruce de directorios tenes que pasar la entrada con filePath.Clean() antes de usarla.

1.9 Deshabilitar listados de directorios.

Si queres deshabilitar el listado de algunos directorios podes hacer lo siguiente:

Agregar un index.html vacio en el directorio especifico que no queres que se liste, lo que pasara es que se enviara ese index en vez de listar el directorio y el usuario obtendra una respuseta 200 OK sin BODY si queres hacer esto con todos los directorios podes hacerlo asi:

$ find ./ui/static -type d -exec touch {}/index.html \;

Otra opcion mas complicada pero mejor, es crear una implementacion de http.FileSystem y retornar os.ErrNotExist para cada directorio.Podes encontrar una explicacion aca

1.10 http.Handler interface.

Cuando usamos un handler, en otras palabras nos estamos refiriendo a un objecto que satisface la interface http.Handler.

type Handler interface {
	ServeHTTP(ResponseWriter, *Request)
}

En palabras mas simples esto significa que para ser un handler un objeto tiene que tener un metodo ServeHTTP() con la firma excacta:

ServeHTTP(http.ResponseWriter, *http.Request)

De esta manera se debería ver un handler en código:

type home struct {}
func (h *home) ServeHTTP(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("This is my home page"))
}

Podemos registrar el handler con servemux asi:

mux := http.NewServeMux()
mux.Handle("/", &home{})

Cuando servemux reciba la solicitud HTTP en ”/”, se llamara al método ServeHTTP de la estructura home que a su vez escribe la respuesta HTTP.

1.10 Handler functions.

Crear un objecto que implemente ServeHTTP() es muy largo y confuso. Entonces, porque en la practica es mas común pasar una función normal como handler? tal como lo hicimos en post anterioes?:

func home(w http.ResponseWriter, r *http.Request) {
	w.Write([]byte("This is my home page"))
}

home es una función normal,y no tiene el método ServeHTTP().Por lo que por si misma no es un handler.

Por eso es que al momento de pasarlo lo inyectamos por el adaptador_http.HandleFunc_ asi:

mux := http.NewServeMux()
mux.Handle("/", http.HandlerFunc(home))

El adaptador http.HandlerFunc() funciona agregando automáticamente el método ServeHTTP a la función home, cuando se ejecuta el método ServeHTTP simplemente llama al contenido de la función original home. Esta es una manera de forzar a una función normal a que cumpla la interfaz http.Handler.

En Post anteriores estuvimos usando el método HandleFunc() para registrar nuestras funciones handler con servemux . Esto es azúcar sintáctico que transforma una función a handler y la registra en un solo paso.

El código anterio es equivalente a esto:

mux := http.NewServeMux()
mux.HandleFunc("/", home)

1.10 Encadenando handlers.

Tal vez habrás notado que en el inicio del proyecto http.ListenAndServe() toma como segundo parámetro un http.Handler

func ListenAndServe(addr string, handler Handler) error

Pero nosotros le pasamos un servemux.

Podemos hacer esto por que servemux también tiene un método ServeHTTP() , lo que significa que satisface la interface http.Handler.

Lo que esta pasando es : Cuando el servidor recibe una petición, llama a servemux en el método ServeHTTP() . Este busca el path correspondiente con la petición y llama al ServeHTTP() del handler. Podes pensar a las aplicaciones web de Go como una cadena de ServeHTTP() que se llaman una después de otra.

1.10 Las peticiones que son manejadas simultáneamente.

Hay otra cosa importante. Todas las peticiones HTTP son servidas en sus propias goroutine , En servidores con mucha carga quiere decir que los handlers están corriendo en simultaneo varias peticiones, lo que hace que tu app sea extremadamente rapida.Lo malo es que tenes que protegerte de las race conditions cuando se accede a recursos compartidos desde los handlers.

Básicamente si dos persona hacen un update a un registro al mismo tiempo y esta corren en simultaneo, cual se vera impactada? esas son los peligros.