DEV Community

Cover image for API Rest con Go (Golang) y PostgreSQL Parte 1
Orlando Monteverde
Orlando Monteverde

Posted on • Edited on

API Rest con Go (Golang) y PostgreSQL Parte 1

Desarrollo de una API Rest sencilla con Go y el motor de base de datos PostgreSQL.

Requisitos:

  • Conocimientos básicos sobre el lenguaje go.
  • Conocimiento sobre HTTP y REST recomendable.
  • Go en su versión 1.11 o superior.
  • PostgreSQL en su versión 9.6 o superior.
  • Conocimientos básicos de SQL deseables.
  • Ganas de aprender.

Que vamos a desarrollar

Crearemos un API Rest para un microblog, estilo twitter en su versión mas básica. Con un modelo de usuarios y uno de publicaciones. Con un CRUD completo en cada caso, autenticación con JSON Web Tokens, creación, uso de middlewares y asegurar contraseña por con bcrypt. ¿Suena interesante? ¡Entonces vamos!

Indice

Crearemos una carpeta para nuestro proyecto

Pueden darle el nombre de su preferencia a la carpeta y colocarla donde les parezca adecuado, en mi caso la llamare go-postgres-microblog para identificarla con facilidad. También puede crearla desde su gestor de archivos, a mi me gusta usar la terminal y sentirme hacker.

mkdir go-postgres-microblog && cd $_
Enter fullscreen mode Exit fullscreen mode

Iniciando un modulo de go

Una vez dentro de la carpeta deseada, desde el terminal escribimos el siguiente comando

go mod init github.com/orlmonteverde/go-postgres-microblog
Enter fullscreen mode Exit fullscreen mode

Eso creara un archivo llamado go.mod y el contenido de la carpeta deberia lucir de esta manera

.
└── go.mod
Enter fullscreen mode Exit fullscreen mode

Si no han trabajando anteriormente con los Go Modules, les recomiendo revisar este articulo, si no desean hacerlo pueden simplemente seguir mis pasos o trabajar en su gopath.

Estructura del proyecto

Para este proyecto estaremos usando de forma un poco libre la estructura propuesta por golang-standards, no es la única estructura posible, pero es una que me resulta particularmente cómoda.

Directorios

  • cmd: para construir los ejecutables (aquí tendríamos tantos directorios con paquetes main como requiera el proyecto).
  • pkg: todos los paquetes que queremos compartir y que puedan ser reutilizables por otros proyectos.
  • internal: paquetes que solo competen al proyecto actual y que no queremos que sean utilizados fuera del mismo.

Creando directorios

Creamos los directorios de trabajo en la raíz del proyecto.

mkdir cmd pkg internal
Enter fullscreen mode Exit fullscreen mode

Deberíamos tener una estructura similar a la siguiente.

.
├── cmd
├── go.mod
├── internal
└── pkg
Enter fullscreen mode Exit fullscreen mode

Ahora dentro de cmd crearemos un directorio que contendrá el punto de inicio de nuestra aplicación, la llamare microblog, pueden llamarla como prefieran, pero para evitar confusiones seria recomendable que mantuvieran los nombres. De hecho, para evitar repetirme constantemente de ahora en adelante todos los nombres utilizados son recomendados, pero no obligatorios, a memos que se indique lo contrario.

mkdir cmd/microblog
Enter fullscreen mode Exit fullscreen mode

Creamos un archivo llamado main.go dentro del directorio microblog .

touch cmd/microblog/main.go
Enter fullscreen mode Exit fullscreen mode

El contenido de este archivo por el momento sera el siguiente

package main

import "fmt"

func main() {
    fmt.Println("Hello world!")
}
Enter fullscreen mode Exit fullscreen mode

Ahora vamos a ejecutar nuestro programa para asegurarnos de que esta todo correcto hasta este punto. Desde la raíz del proyecto, ejecutamos el comando.

go build ./cmd/microblog
Enter fullscreen mode Exit fullscreen mode

Ejecutado el comando deberíamos tener un binario llamado microblog (o microblog.exe si estas en windows). El cual podemos ejecutar.

./microblog
Enter fullscreen mode Exit fullscreen mode

Y obtener una salida como esta.

Hello world!
Enter fullscreen mode Exit fullscreen mode

Todo correcto hasta este punto, así que, comencemos a desarrollar nuestra API.

Modulo server

Lo primero que haremos sera crear un directorio server dentro del directorio internal, y luego crearemos un archivo llamado server.go dentro del mismo.

mkdir internal/server

touch internal/server/server.go
Enter fullscreen mode Exit fullscreen mode

El contenido del archivo server.go sera inicialmente una estructura llamada Server que contendrá un puntero a la estructura http.Server de la biblioteca estándar.

// internal/server/server.go
package server

import "net/http"

// Server is a base server configuration.
type Server struct {
    server *http.Server
}
Enter fullscreen mode Exit fullscreen mode

En este punto vamos a incluir, un paquete de terceros para manejar las rutas, en este caso chi, mediante go get, ejecutando el siguiente comando.

go get github.com/go-chi/chi
Enter fullscreen mode Exit fullscreen mode

Hecho esto, se debió actualizar el archivo go.mod, ademas de crearse un archivo llamado go.sum, no nos preocuparemos de ellos porque no los vamos a editar, no directamente, pero si revisan el go.mod deberían ver el paquete chi que acabamos de descargar como un paquete requerido.

Ahora, regresando al archivo server.go, agregamos una función, que llamaremos New que recibe un string llamado port y retorna un puntero a la estructura Server y un error, dentro de esta función configuraremos el servidor y posteriormente lo retornaremos.

Desde el paquete chi estamos usando la función NewRouter, para obtener un nuevo Router y lo agregamos como Handler en la estructura http.Server, ademas de establecer el Addr con el port que recibe la función y limitar el tiempo de lectura y escritura a 10 segundos, para esto es necesario importar el paquete time de la biblioteca estándar.

// […]

// New inicialize a new server with configuration.
func New(port string) (*Server, error) {
    r := chi.NewRouter()

    serv := &http.Server{
        Addr:         ":" + port,
        Handler:      r,
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    server := Server{server: serv}

    return &server, nil
}
Enter fullscreen mode Exit fullscreen mode

Y ahora agregamos un par de métodos a la estructura Server, uno que llamaremos Close que utilizaremos para cerrar recursos antes de terminar con la ejecución del programa (por ejemplo, la conexión con la de base de datos, aunque de momento este método estará vacío) y uno llamado Start, en el que activamos el servidor.

// […]

// Close server resources.
func (serv *Server) Close() error {
    // TODO: add resource closure.
    return nil
}

// Start the server.
func (serv *Server) Start() {
    log.Printf("Server running on http://localhost%s", serv.server.Addr)
    log.Fatal(serv.server.ListenAndServe())
}
Enter fullscreen mode Exit fullscreen mode

Volvamos al archivo main.go, para requerir el paquete server que acabamos de crear y pongamos a correr el servidor.

package main

import (
    "os"
    "os/signal"

    "log"

    "github.com/orlmonteverde/go-postgres-microblog/internal/server"
)

func main() {
    serv, err := server.New("8000")
    if err != nil {
        log.Fatal(err)
    }

    // start the server.
    go serv.Start()

    // Wait for an in interrupt.
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    <-c

    // Attempt a graceful shutdown.
    serv.Close()
}
Enter fullscreen mode Exit fullscreen mode

Como se puede ver, estamos ejecutando el servidor dentro de una goroutine. Pero si lo dejamos así la función main llegara a su fin y la ejecución del programa terminara, y no es lo que buscamos.

Por ese motivo se agrega un channel que queda en espera de recibir un evento del sistema, una señal de interrupción (CTRL + C), asi el programa se mantendrá en ese punto hasta que ese evento ocurra y una vez que suceda ejecutara el resto de las instrucciones, en nuestro caso el método Close de nuestro Server.

Quizás te estés preguntando porque ejecutar el servidor en un goroutine separada, de no hacerlo no seria necesario crear el channel y demás. Es correcto, pero no tendríamos oportunidad de ejecutar otras instrucciones antes de que el programa finalice. Ahora ejecutamos nuevamente el programa.

go build ./cmd/microblog && ./microblog
Enter fullscreen mode Exit fullscreen mode

Si vamos a http://localhost:8000 en nuestro navegador deberíamos ver un mensaje de “404 page not found”, que puede resultar un poco decepcionante, pero esta bien, significa que tenemos corriendo nuestro servidor, aunque aun no configuramos ninguna ruta, eso lo haremos mas adelante.

Variables de entorno

En el paso anterior, al inicializar el servidor pasamos el puerto directamente como un string, esto funciona bien, pero si quisiéramos cambiar el servidor tendríamos que modificar el código y volver a compilar, ademas de que en algunos casos no tenemos control sobre el puerto con el que funcionara el servidor, como es el caso de plataformas como Heroku.

Ademas de otras variables de configuración, que muy posiblemente queramos cambiar según el entorno en el que este corriendo nuestro programa, como la URI de la base de datos. En esos casos trabajar con variables de entorno podría ser una mejor opción, y eso haremos a continuación.

Primero crearemos un archivo llamado .env en la raíz de nuestro proyecto. Dentro de este archivo agregaremos las variables de entorno de forma =. En este caso solo nos interesa el puerto.

# .env
PORT=8000

Enter fullscreen mode Exit fullscreen mode

Ahora solo debemos leer el valor de la variable de entorno PORT y pasarlo a la función New, así hagamos unos pequeños cambios al inicio de la función main en el archivo main.go.

// cmd/microblog/main.go
// […]
func main() {
    port := os.Getenv("PORT")
    serv, err := server.New(port)
        // [...]
}
Enter fullscreen mode Exit fullscreen mode

Con esto podríamos cargar manualmente nuestras variables de entorno antes de ejecutar el programa y problema resulto, pero es repetitivo, aburrido y fácil de olvidar. Vamos a realizar esto de manera automática por medio de otro paquete de terceros, al igual que con chi tendremos que descargarlo con el comando go get. En este caso el paquete es GoDotEnv

go get github.com/joho/godotenv
Enter fullscreen mode Exit fullscreen mode

Ahora que hemos descargado el paquete, regresamos al archivo main.go para utilizarlo, aunque existen varias formas de trabajar con este paquete y los invito a revisar su documentación, nosotros usaremos la mas sencilla, simplemente importando el paquete autoload. De esta forma, nuestro archivo main.go debería lucir similar al siguiente fragmento de codigo.

package main

import (
    "os"
    "os/signal"

    "log"

    "github.com/orlmonteverde/go-postgres-microblog/internal/server"

    _ "github.com/joho/godotenv/autoload"
)

func main() {
    port := os.Getenv("PORT")
    serv, err := server.New(port)
    if err != nil {
        log.Fatal(err)
    }

    // start the server.
    go serv.Start()

    // Wait for an in interrupt.
    c := make(chan os.Signal, 1)
    signal.Notify(c, os.Interrupt)
    <-c

    // Attempt a graceful shutdown.
    serv.Close()
}
Enter fullscreen mode Exit fullscreen mode

Con estos cambios, podemos modificar el valor de la variable de entornos tantas veces como sean necesarias y al ejecutar el programa este tomara el nuevo valor. Los invito a hacer la prueba.

Por ahora lo dejaremos hasta aquí, en el siguiente articulo continuaremos con el desarrollo de nuestra API. Les comparto el repositorio con el código del proyecto hasta el momento.

Realmente espero que el contenido les sea de utilidad, y cualquier duda o sugerencia, por favor, déjenla en los comentarios y les responderé tan pronto como me resulte posible. Saludos y gracias por leer.

konosuba

Top comments (3)

Collapse
 
german147ss profile image
German Hugo Martinez Mendieta

¡Buenas Orlando! No veo necesaria tu go serv.Start() , ya que mismo en el metodo ListenAndServe() crea una Go routine que mantiene el canal principal.

Sacando el manejo de la Go routine externo y de ese canal, funciona perfectamente

Collapse
 
dbertaso profile image
Diego Bertaso

Saludos Sr. Orlando Monteverde, espero se encuentre bien.

Estoy siguiendo su post para crear una api rest con golang y postgresql y se me presenta el siguiente error en la primera parte del post durante el "go build": expected declaration, found 'go' en la instrucción "go serv.Start()" no se si se deba a un error en el documento o es que ha habido un cambio en la nueva version de golang.

Le incluyo el código de los archivos ** main.go** y server.go

*main.go *

package main

import (

"os"
"os/signal"
"log"
"localhost.com/go_postgres_microblog/internal/server"
Enter fullscreen mode Exit fullscreen mode

)

func main() {

    serv, err := sever.New("8000")
    if err != nil {
        log.Fatal(err)
    }
}

// start the server
go serv.Start()
Enter fullscreen mode Exit fullscreen mode

// Wait for an interrupt
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt)
<-const

//Attempt a graceful shutdown

serv.Close()
Enter fullscreen mode Exit fullscreen mode

server.go

// internal/server/server.go

package server

import (
"log"
"net/http"
"time"

"github.com/go-chi/chi"
Enter fullscreen mode Exit fullscreen mode

)

// Server is a base server configuration

type Server struct {
server *http.Server
}

// New inicialize a new server with configuration

func New(port string) (*Server, error) {

r := chi.NewRouter()

serv := &http.Server{
    Addr:         ":" + port,
    Handler:      r,
    ReadTimeout:  10 * time.Second,
    WriteTimeout: 10 * time.Second,
}

server = Server{server: serv}

return &server, nil
Enter fullscreen mode Exit fullscreen mode

}

// Close server resources

func (serv *Server) Close() error {
// TODO: add resource closure.
return nil
}

// Start the server.
func (serv *Server) Start() {
log.Printf("Server running on localhost%s", serv.server.Addr)
log.Fatal(serv.server.ListenAndServe())
}

Quedo atento a sus comentarios.
Muchas Gracias.

Diego Bertaso
@dbertaso
@dbertaso@gmail.com

Collapse
 
superterro666 profile image
superterro666

Hola que tal, ante todo gracias por el tutorial, pero me han surgido unos problemas, el primero de todos es que en el repositorio el archivo .env esta en la raíz, pero no me lo coge, tengo que ponerlo junto a main.go, no se por que puede ser.
Para el segundo problema, cambie username por email para la validacion, cree un nuevo interface y un nuevo metodo no creo que se deba a eso el error.
El problema es cuando hago login la variable u.PasswordHash esta vacia al igual que la variable u.Password, sin embargo las otras no están vacías, en postgre el campo password esta lleno, no se a que se puede deber, por otro lado desde postman las peticiones funcionan bien, pero al hacerlo desde una aplicación angular da problemas con el CORS, en que archivo debería habilitarlo.
Perdon por el tocho.