2 Configuración y manejo de errores

En esta sección vamos a aprender:

  • Establecer los ajustes de configuración para la aplicación en tiempo de ejecución usando flags en la linea de comandos.
  • Mejorar los mensajes de log, para incluir mas información y mensajes diferentes dependiendo del tipo o nivel del mensaje.
  • Hacer que las dependencias estén disponibles en tus handlers de manera que sean expandibles,type-safe (de tipo seguro) y fáciles de testear.
  • Centralizar el manejo de errores para que no tengamos código repetido.

2.1 Manejando las configuraciones para las opciones

Nuestra aplicación tiene un par de configuraciones que tenemos hard-codeadas:

  • La configuración de la dirección del servidor (“:4000”)
  • La ruta de los archivos estáticos (“./ui/static”)

Tener esas cosas hard-codeadas no es lo ideal, no hay una separación entre nuestras configuraciones y nuestro código ademas de que no podemos cambiar las opciones en runtime ( lo cual es importante para cambiar entre configuraciones de desarrollo, testing y producción).

En este post vamos a empezar a mejorar eso, y a hacer que la configuración de nuestro server sea configurable en runtime.

2.1 Command-line flags

En Go una manera de manejar las configuraciones mediante linea de comandos es usando flags de la siguiente manera:

$ go run ./cmd/web -addr=":80"

Una manera sencilla de aceptar configuraciones mediante linea de comandos en nuestra aplicación seria con algo como esto:

addr := flag.String("addr", ":4000", "HTTP network address")

Lo que estamos haciendo es habilitar un flag addr con un valor por default :4000 y una descripción.

Usemoslo en nuestra aplicación.

package main
import (
+"flag"
"log"
"net/http"
)
func main() {
+    // Define a new command-line flag with the name 'addr', a default value of ":4000"
+    // and some short help text explaining what the flag controls. The value of the
+    // flag will be stored in the addr variable at runtime.
+    addr := flag.String("addr", ":4000", "HTTP network address")
+    // Importantly, we use the flag.Parse() function to parse the command-line flag.
+    // This reads in the command-line flag value and assigns it to the addr
+    // variable. You need to call this *before* you use the addr variable
+    // otherwise it will always contain the default value of ":4000". If any errors are
+    // encountered during parsing the application will be terminated.
+    flag.Parse()
    mux := http.NewServeMux()
    fileServer := http.FileServer(http.Dir("./ui/static/"))
    mux.Handle("/static/", http.StripPrefix("/static", fileServer))
    mux.HandleFunc("/", home)
    mux.HandleFunc("/snippet/view", snippetView)
    mux.HandleFunc("/snippet/create", snippetCreate)
+    // The value returned from the flag.String() function is a pointer to the flag
+    // value, not the value itself. So we need to dereference the pointer (i.e.
+    // prefix it with the * symbol) before using it. Note that we're using the
+    // log.Printf() function to interpolate the address with the log message.
+    log.Printf("Starting server on %s", *addr)
+    err := http.ListenAndServe(*addr, mux)
    log.Fatal(err)
}

Guardamos y corremos nuestro servidor asi :

$ go run ./cmd/web -addr=":9999"
2022/01/29 15:50:20 Starting server on :9999

Nota: Los puertos 0-1023 están restringidos y (normalmente) sólo pueden ser utilizados por servicios que tengan privilegios de raíz. Si intenta utilizar uno de estos puertos, debería obtener un bind: mensaje de error de permiso denegado al inicio.

2.1 Default values

El flag -addr es opcional , si no lo ponemos caerá en el valor por defecto que pusimos al momento de definir el_flag_.

$ go run ./cmd/web
2022/01/29 15:51:36 Starting server on :4000

2.1 Type convertions

En el código anterior usamos flag.String() para definir el flag del comand-line.El cual nos sirvió para convertir el valor que nos envía el usuario (en este caso nosotros) a string, si el valor no puede ser convertido arrojara un error y saldrá de la app.

Go tambien tiene otro tipo de funciones que nos sirven para convertir a otro tipo de datos como lo es flag.Int(),flag.Bool y flag.Float64(), todos ellos trabajan de la misma manera que lo hace flag.String().

2.1 Ayudas automatizadas

Otra característica es qeu podemos usar el flag -help para listar todas las comand-line que hayamos configurado.

$ go run ./cmd/web -help
Usage of /tmp/go-build3672328037/b001/exe/web:
-addr string
HTTP network address (default ":4000")

2.1 Información adicional

2.1 Variables de entorno

Si queres podes configurar tus variables de entorno y acceder a ellas directamente desde la app con os.Getenv() asi:

addr := os.Getenv("SNIPPETBOX_ADDR")

Pero tiene algunas desventajas comparado con las line-flags.

  • No podes definir un valor por defecto, os.Getenv() devolverá un string vació si no encuentra la variable de entorno.
  • No tenemos acceso al flag -help
  • os.Getenv() siempre retorna un string.

Para tener lo mejor de ambos mundos podes hacer esto en tu line de comandos:

$ export SNIPPETBOX_ADDR=":9999"
$ go run ./cmd/web -addr=$SNIPPETBOX_ADDR
2022/01/29 15:54:29 Starting server on :9999
2.1 Boolean flags

Para las flags definidas con flag.Bool() podemos omitr el valor -flag=true , el siguiente codigo hace lo mismo:

$ go run ./example -flag=true
$ go run ./example -flag

Solo tenes que usar -flag=false si queres que el valor se setee en false

2.1 Valores pre-existentes

Los valores analizados se pueden almacenar directamente en las direcciones de memoria de variables preexistentes utilizando las funciones flag.StringVar(), flag.IntVar(), flag.BoolVar() y similares. Esto es útil cuando deseas almacenar todas tus configuraciones en una sola estructura (struct).

type config struct {
    addr string
    staticDir string
}
...
var cfg config
flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP network address")
flag.StringVar(&cfg.staticDir, "static-dir", "./ui/static", "Path to static assets")
flag.Parse()
  1. Definición de la estructura de configuración:
type config struct {
    addr      string
    staticDir string
}

Aquí se define una estructura llamada config con dos campos: addr (dirección) y staticDir (directorio estático).

  1. Creación de una instancia de la estructura:
var cfg config

Se crea una variable llamada cfg del tipo config. Esta será utilizada para almacenar las configuraciones analizadas desde la línea de comandos.

  1. Análisis de los argumentos de la línea de comandos:
flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP network address")
flag.StringVar(&cfg.staticDir, "static-dir", "./ui/static", "Path to static assets")
flag.Parse()
  • flag.StringVar(&cfg.addr, "addr", ":4000", "HTTP network address"): Esto indica que el valor del argumento addr se analizará y se almacenará en la variable cfg.addr. El valor predeterminado es ":4000", y la descripción es “HTTP network address”.
  • flag.StringVar(&cfg.staticDir, "static-dir", "./ui/static", "Path to static assets"): Esto indica que el valor del argumento static-dir se analizará y se almacenará en la variable cfg.staticDir. El valor predeterminado es "./ui/static", y la descripción es “Path to static assets”.
  • flag.Parse(): Este comando realiza el análisis real de los argumentos pasados por línea de comandos y asigna los valores correspondientes a las variables configuradas anteriormente.

En resumen, este código permite que los valores pasados por línea de comandos, como --addr :8080 o --static-dir /path/to/assets, se almacenen directamente en las variables cfg.addr y cfg.staticDir de la estructura config.

Este enfoque es útil para centralizar la configuración en una estructura y analizar los valores de la línea de comandos de manera más organizada y eficiente.

2.2 Registro estructurado

En este momento, estamos generando entradas de registro desde nuestro código utilizando las funciones log.Printf() y log.Fatal(). Un buen ejemplo de esto es la entrada de registro “iniciando servidor…” que imprimimos justo antes de que nuestro servidor comience:

$ go run ./cmd/web/
2023/09/05 11:12:35 starting server on :4000

Para muchas aplicaciones, utilizar el registrador estándar será suficiente, y no hay necesidad de hacer algo más complejo.

Pero para aplicaciones que generan una gran cantidad de registros, es posible que desees hacer que las entradas de registro sean más fáciles de filtrar y trabajar con ellas. Por ejemplo, es posible que desees distinguir entre diferentes niveles de gravedad de las entradas de registro (como entradas informativas y de error), o imponer una estructura coherente para las entradas de registro de modo que sean fáciles de analizar por programas o servicios externos.

Para admitir esto, la biblioteca estándar de Go incluye el paquete log/slog, que te permite crear registradores estructurados personalizados que generan entradas de registro en un formato específico. Cada entrada de registro incluye las siguientes cosas:

  • Un sello de tiempo con precisión en milisegundos.
  • El nivel de gravedad de la entrada de registro (Depuración, Información, Advertencia o Error).
  • El mensaje de registro (un valor de cadena arbitrario).
  • Opcionalmente, cualquier cantidad de pares clave-valor (conocidos como atributos) que contengan información adicional.

2.2 Creando una estructura logger

El código para crear un registrador estructurado con el paquete log/slog puede ser un poco confuso la primera vez que lo ves. Lo importante a entender es que todos los registradores estructurados tienen un controlador de registro estructurado asociado a ellos (no debe confundirse con un controlador HTTP), y es este controlador el que controla cómo se formatean las entradas de registro y dónde se escriben.

El código para crear un registrador se ve de la siguiente manera:

loggerHandler := slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{...})
logger := slog.New(loggerHandler)

En la primera línea de código, primero utilizamos la función slog.NewTextHandler() para crear el controlador de registro estructurado. Esta función acepta dos argumentos:

  • El primer argumento es el destino de escritura para las entradas de registro. En el ejemplo anterior, lo hemos establecido en os.Stdout, lo que significa que escribirá las entradas de registro en la corriente estándar de salida (stdout).
  • El segundo argumento es un puntero a una estructura slog.HandlerOptions, que puedes utilizar para personalizar el comportamiento del controlador. Veremos algunas de las personalizaciones disponibles al final de este capítulo. Si estás satisfecho con los valores predeterminados y no deseas cambiar nada, puedes pasar nil como el segundo argumento en su lugar.

Luego, en la segunda línea de código, realmente creamos el registrador estructurado al pasar el controlador a la función slog.New().

logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{...}))

2.2 Usando la estructura logger

logger.Info("request received")

Resultaría en una entrada de registro que se ve así:

time=2023-09-05T11:21:37.311+02:00 level=INFO msg="request received"

Los métodos Debug(), Info(), Warn() o Error() son métodos variádicos que aceptan un número arbitrario de atributos adicionales (pares clave-valor). De la siguiente manera:

logger.Info("request received", "method", "GET", "path", "/")

En este ejemplo, hemos agregado dos atributos adicionales a la entrada de registro: la clave “method” y el valor “GET”, y la clave “path” y el valor ”/“. Las claves de los atributos deben ser siempre cadenas, pero los valores pueden ser de cualquier tipo. En este ejemplo, la entrada de registro se verá así:

time=2023-09-05T11:22:30.405+02:00 level=INFO msg="request received" method=GET path=/

Si las claves de tus atributos, los valores o el mensaje de registro contienen caracteres ” o = o cualquier espacio en blanco, serán encerrados entre comillas dobles en la salida del registro. Podemos ver este comportamiento en el ejemplo anterior, donde el mensaje de registro msg=“request received” está entre comillas.

2.2 Añadiendo registro estructurado a nuestra aplicación

De acuerdo, procedamos a actualizar nuestro archivo main.go para usar un registrador estructurado en lugar del registrador estándar de Go. De la siguiente manera:

package main
import (
    "flag"
    "log/slog"
    "net/http"
    "os"
)
func main() {
    addr := flag.String("addr", ":4000", "HTTP network address")
    flag.Parse()
    // Use the slog.New() function to initialize a new structured logger, which
    // writes to the standard out stream and uses the default settings.
    logger := slog.New(slog.NewTextHandler(os.Stdout, nil))
    mux := http.NewServeMux()
    fileServer := http.FileServer(http.Dir("./ui/static/"))
    mux.Handle("/static/", http.StripPrefix("/static", fileServer))
    mux.HandleFunc("/", home)
    mux.HandleFunc("/snippet/view", snippetView)
    mux.HandleFunc("/snippet/create", snippetCreate)
    // Use the Info() method to log the starting server message at Info severity
    // (along with the listen address as an attribute).
    logger.Info("starting server", "addr", *addr)
    err := http.ListenAndServe(*addr, mux)
    // And we also use the Error() method to log any error message returned by
    // http.ListenAndServe() at Error severity (with no additional attributes),
    // and then call os.Exit(1) to terminate the application with exit code 1.
    logger.Error(err.Error())
    os.Exit(1)
}

No existe un equivalente de registro estructurado para la función log.Fatal() que podamos usar para manejar un error devuelto por http.ListenAndServe(). En su lugar, lo más cercano que podemos hacer es registrar un mensaje en el nivel de gravedad Error y luego llamar manualmente a os.Exit(1) para terminar la aplicación con el código de salida 1, como estamos haciendo en el código anterior.

Adelante, ejecuta la aplicación y luego abre otra ventana de terminal e intenta ejecutarla una segunda vez. Esto debería generar un error porque la dirección de red en la que nuestro servidor quiere escuchar (“:4000”) ya está en uso.

La salida de registro en tu segunda terminal debería verse algo así:

$ go run ./cmd/web
time=2023-09-05T17:39:47.287+02:00 level=INFO msg="starting server" addr=:4000
time=2023-09-05T17:39:47.287+02:00 level=ERROR msg="listen tcp :4000: bind: address already in use"
exit status 1

La primera entrada de registro tiene el nivel de gravedad INFO y el mensaje msg=“starting server”, junto con el atributo adicional addr=:4000. En contraste, vemos que la segunda entrada de registro tiene el nivel de gravedad ERROR, el valor msg contiene el contenido del mensaje de error y no hay atributos adicionales.

2.2 Informacion adicional

2.2 Atributos más seguros
logger.Info("starting server", "addr") // Oops, the value for "addr" is missing

Cuando esto sucede, la entrada de registro seguirá siendo escrita, pero el atributo tendrá la clave !BADKEY, de la siguiente manera:

time=2023-09-05T17:41:19.227+02:00 level=INFO msg="starting server" !BADKEY=addr

Para evitar que esto suceda y detectar cualquier problema en tiempo de compilación, puedes utilizar la función slog.Any() para crear un par de atributos en su lugar:

logger.Info("starting server", slog.Any("addr", ":4000"))

O puedes ir aún más lejos e introducir un mayor nivel de seguridad de tipos utilizando las funciones slog.String(), slog.Int(), slog.Bool(), slog.Time() y slog.Duration() para crear atributos con un tipo de valor específico.

logger.Info("starting server", slog.String("addr", ":4000"))

Ya sea que desees utilizar estas funciones o no, depende de ti. El paquete log/slog es relativamente nuevo en Go (introducido en Go 1.21), y aún no existen muchas prácticas recomendadas o convenciones establecidas para su uso. Sin embargo, el equilibrio es sencillo: usar funciones como slog.String() para crear atributos es más verbose, pero más seguro en el sentido de que reduce el riesgo de errores en tu aplicación.

2.2 Registros con formato JSON

La función slog.NewTextHandler() que hemos utilizado en este capítulo crea un controlador que escribe entradas de registro en formato de texto sin formato. Sin embargo, es posible crear un controlador que escriba las entradas de registro como objetos JSON en su lugar utilizando la función slog.NewJSONHandler(). De la siguiente manera:

logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))

Cuando se utiliza el controlador JSON, la salida de registro se verá similar a esto:

{"time":"2023-09-05T17:42:33.028876279+02:00","level":"INFO","msg":"starting server","addr":":4000"}
{"time":"2023-09-05T17:42:33.028997918+02:00","level":"ERROR","msg":"listen tcp :4000: bind: address already in use"}

Como hemos mencionado varias veces, el paquete log/slog admite cuatro niveles de gravedad: Debug, Info, Warn y Error, en ese orden. Debug es el nivel menos grave, y Error es el más grave. Por defecto, el nivel mínimo de registro para un registrador estructurado es Info. Esto significa que todas las entradas de registro con una gravedad menor que Info, es decir, las entradas de nivel Debug, se descartarán silenciosamente. Puedes utilizar la estructura slog.HandlerOptions para anular esto y configurar el nivel mínimo en Debug (u cualquier otro nivel) si lo deseas:

logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))

2.2 Caller location

También puedes personalizar el controlador para que incluya el nombre del archivo y el número de línea del código fuente que realiza la llamada en las entradas de registro, de la siguiente manera:

logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
AddSource: true,
}))

Las entradas de registro se verán similares a esto, con la ubicación del llamante registrada bajo la clave “source”:

time=2023-09-05T17:43:52.373+02:00 level=INFO source=/home/alex/code/snippetbox/cmd/web/main.go:32 msg="starting server" addr=:4000

2.2 Registro desacoplado

En este capítulo, hemos configurado nuestro registrador estructurado para escribir entradas en os.Stdout, que es la corriente estándar de salida. La gran ventaja de escribir entradas de registro en os.Stdout es que tu aplicación y el registro están desacoplados. Tu aplicación en sí no se preocupa por la dirección o el almacenamiento de los registros, lo que facilita la gestión de los registros de manera diferente según el entorno. Durante el desarrollo, es fácil ver la salida del registro porque se muestra en la terminal. En entornos de puesta en escena o producción, puedes redirigir la corriente hacia un destino final para su visualización y archivo. Este destino podría ser archivos en disco o un servicio de registro como Splunk. De cualquier manera, el destino final de los registros puede ser gestionado por tu entorno de ejecución de manera independiente de la aplicación. Por ejemplo, podríamos redirigir la corriente estándar de salida a un archivo en disco al iniciar la aplicación de la siguiente manera:

$ go run ./cmd/web >>/tmp/web.log

Usar la doble flecha >> agregará al final de un archivo existente en lugar de truncarlo al iniciar la aplicación.

2.2 Concurrent logging

Los registradores personalizados creados por slog.New() son seguros para la concurrencia. Puedes compartir un solo registrador y usarlo en múltiples goroutines y en tus controladores HTTP sin preocuparte por las condiciones de carrera. Dicho esto, si tienes varios registradores estructurados escribiendo en el mismo destino, debes tener cuidado y asegurarte de que el método Write() subyacente del destino también sea seguro para su uso concurrente.

2.3 Inyección de dependencias

Tenemos otro problema con los logs, si vemos los handlers que fuimos creando podremos ver que el handler de home no esta escribiendo las respuestas de los log con errLog ni infoLog

func home(w http.ResponseWriter, r *http.Request) {
    ...
    ts, err := template.ParseFiles(files...)
    if err != nil {
    log.Print(err.Error()) // This isn't using our new error logger.
    http.Error(w, "Internal Server Error", 500)
    return
    }
    err = ts.ExecuteTemplate(w, "base", nil)
    if err != nil {
    log.Print(err.Error()) // This isn't using our new error logger.
    http.Error(w, "Internal Server Error", 500)
    }
}

La pregunta es, como podemos hacer para que los logs que creamos nosotros mismos sean accesibles desde nuestra función home que tenemos en el main()?

Como a lo largo de una aplicación web se necesita que muchas dependencias sean accedidas por los handlers la otra pregunta seria . Como podemos hacer para que cualquier dependencia sea accesible para nuestros handlers?

Hay varias maneras de abordar esto. La mas simple es poner las dependencias en variables globales.Pero en general es buena practica inyectar dependencias en los handlers esto hace que tu codigo sea mas explicito , menos propenso a errores y mas fáciles de testear .

Para aplicaciones donde todos tus handlers están bajo el mismo paquete, como nuestra app,una manera de inyectar las dependencias es ponerlas en una struct “application” y definir a los handlers como metodos de esta estructura, asi:

cmd/web/handlers.go

package main
import (
    "flag"
    "log"
    "net/http"	
    "os"
)
// Define an application struct to hold the application-wide dependencies for the
// web application. For now we'll only include fields for the two custom loggers, but
// we'll add more to it as the build progresses.
type application struct {
    errorLog *log.Logger
    infoLog *log.Logger
}
func main() {
	...
}

Luego actualizamos los handlers para que sean métodos de la estructura application

package main
import (
    "fmt"
    "html/template"
    "net/http"
    "strconv"
)
// Change the signature of the home handler so it is defined as a method against
// *application.
func (app *application) home(w http.ResponseWriter, r *http.Request) {
    if r.URL.Path != "/" {
        http.NotFound(w, r)
        return
    }
    files := []string{
    "./ui/html/base.tmpl",
    "./ui/html/partials/nav.tmpl",
    "./ui/html/pages/home.tmpl",
    }
    ts, err := template.ParseFiles(files...)
    if err != nil {
        // Because the home handler function is now a method against application
        // it can access its fields, including the error logger. We'll write the log
        // message to this instead of the standard logger.
        app.errorLog.Print(err.Error())
        http.Error(w, "Internal Server Error", 500)
        return
    }
    err = ts.ExecuteTemplate(w, "base", nil)
    if err != nil {
        // Also update the code here to use the error logger from the application
        // struct.
        app.errorLog.Print(err.Error())
        http.Error(w, "Internal Server Error", 500)
    }
    }
    // Change the signature of the snippetView handler so it is defined as a method
    // against *application.
    func (app *application) 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)
    }
    // Change the signature of the snippetCreate handler so it is defined as a method
    // against *application.
    func (app *application) 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..."))
}

En main.go

package main
import (
 "flag"
 "log"
 "net/http"
 "os"
)
type application struct {
errorLog *log.Logger
infoLog
*log.Logger
}
func main() {
    addr := flag.String("addr", ":4000", "HTTP network address")
    flag.Parse()
    infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
    errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
    // Initialize a new instance of our application struct, containing the
    // dependencies.
    app := &application{
        errorLog: errorLog,
        infoLog:
        infoLog,
    }
    // Swap the route declarations to use the application struct's methods as the
    // handler functions.
    mux := http.NewServeMux()
    fileServer := http.FileServer(http.Dir("./ui/static/"))
    mux.Handle("/static/", http.StripPrefix("/static", fileServer))
    mux.HandleFunc("/", app.home)
    mux.HandleFunc("/snippet/view", app.snippetView)
    mux.HandleFunc("/snippet/create", app.snippetCreate)
    srv := &http.Server{
        Addr:*addr,
        ErrorLog: errorLog,
        Handler: mux,
    }
    infoLog.Printf("Starting server on %s", *addr)
    err := srv.ListenAndServe()
    errorLog.Fatal(err)
}

2.3 Información adicional

2.3 Closures para inyección de dependencias

El patron que usamos anteriormente para inyectar las dependencias no funciona si los handlers estan dispersos por multiples paquetes.En ese caso la alternativa es crear un paquete config exportar una struct Application y pasar la instancia como parametro al handler:

func main() {
    app := &config.Application{
    	ErrorLog: log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
    }
    mux.Handle("/", examplePackage.ExampleHandler(app))
}	
func ExampleHandler(app *config.Application) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        ...
        ts, err := template.ParseFiles(files...)
        if err != nil {
            app.ErrorLog.Print(err.Error())
            http.Error(w, "Internal Server Error", 500)
            return
        }
    ...
    }
}

Ejemplo de como usar el patron closure

2.4 Centralizar el manejo de errores

Vamos a ordenar nuestra aplicación moviendo algunos manejos de errores en métodos helpers, esto nos ayudara a separar responsabilidades y para parar de repetir código a medida que continuamos desarrollando nuestra app.

Comencemos creando un archivo llamado helpers.go en cmd/web

package main

import (
	"net/http"
)

// The serverError helper writes a log entry at Error level (including the request
// method and URI as attributes), then sends a generic 500 Internal Server Error
// response to the user.
func (app *application) serverError(w http.ResponseWriter, r *http.Request, err error) {
	var (
		method = r.Method
		uri    = r.URL.RequestURI()
	)
	app.logger.Error(err.Error(), "method", method, "uri", uri)
	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

// The clientError helper sends a specific status code and corresponding description
// to the user. We'll use this later in the book to send responses like 400 "Bad
// Request" when there's a problem with the request that the user sent.
func (app *application) clientError(w http.ResponseWriter, status int) {
	http.Error(w, http.StatusText(status), status)
}

// For consistency, we'll also implement a notFound helper. This is simply a
// convenience wrapper around clientError which sends a 404 Not Found response to
// the user.
func (app *application) notFound(w http.ResponseWriter) {
	app.clientError(w, http.StatusNotFound)
}

En este código también hemos introducido otra cosa nueva: la función http.StatusText(). Esta función devuelve una representación de texto amigable para humanos de un código de estado HTTP dado. Por ejemplo, http.StatusText(400) devolverá la cadena “Bad Request” y http.StatusText(500) devolverá la cadena “Internal Server Error”.

Ahora que eso está hecho, regresa a tu archivo handlers.go y actualízalo para usar los nuevos helpers.

package main

import (
	"fmt"
	"html/template"
	"net/http"
	"strconv"
)

func (app *application) home(w http.ResponseWriter, r *http.Request) {
	if r.URL.Path != "/" {
		app.notFound(w) // Use the notFound() helper
		return
	}
	files := []string{
		"./ui/html/base.tmpl",
		"./ui/html/partials/nav.tmpl",
		"./ui/html/pages/home.tmpl",
	}
	ts, err := template.ParseFiles(files...)
	if err != nil {
		app.serverError(w, r, err) // Use the serverError() helper.
		return
	}
	err = ts.ExecuteTemplate(w, "base", nil)
	if err != nil {
		app.serverError(w, r, err) // Use the serverError() helper.
	}
}
func (app *application) snippetView(w http.ResponseWriter, r *http.Request) {
	id, err := strconv.Atoi(r.URL.Query().Get("id"))
	if err != nil || id < 1 {
		app.notFound(w) // Use the notFound() helper.
		return
	}
	fmt.Fprintf(w, "Display a specific snippet with ID %d...", id)
}
func (app *application) snippetCreate(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		w.Header().Set("Allow", http.MethodPost)
		app.clientError(w, http.StatusMethodNotAllowed) // Use the clientError() helper.
		return
	}
	w.Write([]byte("Create a new snippet..."))
}

Cuando hayas actualizado eso, reinicia tu aplicación y realiza una solicitud a http://localhost:4000 en tu navegador. Nuevamente, esto debería provocar un (intencionado) error y deberías ver la entrada de registro correspondiente en tu terminal, incluyendo el método de solicitud y la URI como atributos.

$ go run ./cmd/web
time=2023-09-05T18:02:12.454+02:00 level=INFO msg="starting server" addr=:4000
time=2023-09-05T18:02:18.316+02:00 level=ERROR msg="open ./ui/html/pages/home.tmpl: no such file or directory" method=GET uri=/

2.4 Informacion adicional

2.4 Rastreo de la pila

Puedes utilizar la función debug.Stack() para obtener un rastreo de la pila que describe la ruta de ejecución de la aplicación para la goroutine actual. Incluir esto como un atributo en tus entradas de registro puede ser útil para depurar errores.

Si lo deseas, puedes actualizar el método serverError() para que incluya un rastreo de pila en las entradas de registro de la siguiente manera:

package main

import (
	"net/http"
	"runtime/debug"
)

func (app *application) serverError(w http.ResponseWriter, r *http.Request, err error) {
	var (
		method = r.Method
		uri    = r.URL.RequestURI()
		// Use debug.Stack() to get the stack trace. This returns a byte slice, which
		// we need to convert to a string so that it's readable in the log entry.
		trace = string(debug.Stack())
	)
	// Include the trace in the log entry.
	app.logger.Error(err.Error(), "method", method, "uri", uri, "trace", trace)
	http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}

La salida de la entrada de registro se vería entonces algo así (se han agregado saltos de línea para mayor legibilidad):

time=2023-09-05T18:06:39.965+02:00 level=ERROR msg="open ./ui/html/pages/home.tmpl:
no such file or directory" method=GET uri=/ trace="goroutine 6 [running]:\nruntime/
debug.Stack()\n\t/usr/local/go/src/runtime/debug/stack.go:24 +0x5e\nmain.(*applicat
ion).serverError(0xc00006c048, {0x8221b0, 0xc0000f40e0}, 0x3?, {0x820600, 0xc0000ab
5c0})\n\t/home/alex/code/snippetbox/cmd/web/helpers.go:14 +0x74\nmain.(*application
).home(0x10?, {0x8221b0?, 0xc0000f40e0}, 0xc0000fe000)\n\t/home/alex/code/snippetbo
x/cmd/web/handlers.go:24 +0x16a\nnet/http.HandlerFunc.ServeHTTP(0x4459e0?, {0x8221b
0?, 0xc0000f40e0?}, 0x6cc57a?)\n\t/usr/local/go/src/net/http/server.go:2136 +0x29\n
net/http.(*ServeMux).ServeHTTP(0xa7fde0?, {0x8221b0, 0xc0000f40e0}, 0xc0000fe000)\n
\t/usr/local/go/src/net/http/server.go:2514 +0x142\nnet/http.serverHandler.ServeHTT
P({0xc0000aaf00?}, {0x8221b0?, 0xc0000f40e0?}, 0x6?)\n\t/usr/local/go/src/net/http/
server.go:2938 +0x8e\nnet/http.(*conn).serve(0xc0000c0120, {0x8229e0, 0xc0000aae10})
\n\t/usr/local/go/src/net/http/server.go:2009 +0x5f4\ncreated by net/http.(*Server).
Serve in goroutine 1\n\t/usr/local/go/src/net/http/server.go:3086 +0x5cb\n"

2.5 Aislando las Rutas

En este momento tenemos nuestras rutas en main.go pero lo optimo seria tener el codigo de manera clara y organizada. Para mejorar eso vamos a separar nuestras rutas en un archivo routes.go :

$ cd $HOME/code/snippetbox
$ touch cmd/web/routes.go
package main
import "net/http"
// The routes() method returns a servemux containing our application routes.
func (app *application) routes() *http.ServeMux {
    mux := http.NewServeMux()
    fileServer := http.FileServer(http.Dir("./ui/static/"))
    mux.Handle("/static/", http.StripPrefix("/static", fileServer))
    mux.HandleFunc("/", app.home)
    mux.HandleFunc("/snippet/view", app.snippetView)
    mux.HandleFunc("/snippet/create", app.snippetCreate)
    return mux
}

Nuestro main.go

package main
...
func main() {
    
    addr := flag.String("addr", ":4000", "HTTP network address")
    flag.Parse()
    
    infoLog := log.New(os.Stdout, "INFO\t", log.Ldate|log.Ltime)
    errorLog := log.New(os.Stderr, "ERROR\t", log.Ldate|log.Ltime|log.Lshortfile)
    
    app := &application{
        errorLog: errorLog,
        infoLog: infoLog,
    }
    
-    mux := http.NewServeMux()
-     fileServer := http.FileServer(http.Dir("./ui/static/"))

- 	 mux.Handle("/static/", http.StripPrefix("/static/", fileServer))
  
-  	 mux.HandleFunc("/", app.home)
-  	 mux.HandleFunc("/snippet/view", app.snippetView)
-  	 mux.HandleFunc("/snippet/create", app.snippetCreate)

    srv := &http.Server{
        Addr:*addr,
        ErrorLog: errorLog,
-        Handler: mux,
+        // Call the new app.routes() method to get the servemux containing our routes.
+        Handler: app.routes(),
    }
    
    infoLog.Printf("Starting server on %s", *addr)
    err := srv.ListenAndServe()
    errorLog.Fatal(err)
}

Las rutas de nuestra aplicación están ahora aisladas y encapsuladas en app.routes() y la responsabilidad de nuestro main() esta limitado a:

  • Parsear las configuraciones de la aplicación en tiempo de ejecución (runtime)
  • Establecer las dependencias para los handlers
  • Correr el servidor HTTP