Hoy veremos cómo consumir una base de datos de MySQL o MariaDB (crear un CRUD) utilizando uno de mis lenguajes de programación favoritos: Go, también conocido como GoLang.
Lo que haremos será un CRUD a través de la consola; esto para mantener la simplicidad, pero recuerda que Go sirve para muchísimas cosas más (Sublime POS está escrito en Go).
Recuerda que para esto debes tener instalado MySQL, si lo deseas, puedes instalarlo con XAMPP. También recuerda instalar y configurar Go.
Por otro lado, te invito a leer algunos ejercicios de MySQL y cómo conectarte a la CLI del servidor MySQL.
Explicación de lo que haremos
Al terminar este post, tendremos una pequeñísima app que registre contactos. Algo así como una agenda, en donde tendremos el nombre, la dirección y el correo electrónico. Para ello implementaremos las operaciones básicas como son:
- Conexión a MySQL desde Go
- Crear datos con insert
- Leer datos de una tabla de MySQL con select
- Actualizar datos existentes de MySQL
- Eliminar un dato de una tabla de MySQL a través de su ID
Esto me recuerda a que ya hice un ejemplo de MySQL y Python hace algún tiempo.
Instalar librería para conectar Go con MySQL
Ejecutamos este comando para instalar la librería:
go get -u github.com/go-sql-driver/mysql
Por cierto, pásate por el repositorio de GitHub a dejarle una estrella o a contribuir. Ah, también debes tener instalado git en tu PATH, mira aquí cómo hacerlo en Windows.
Estructura de la tabla
No vamos a ver buenas prácticas de bases de datos, ni relaciones, ni cosas de esas. Haremos nuestra estructura lo mejor posible, pero no enfocándonos en la misma.
create database if not exists agenda;
use agenda;
create table if not exists agenda(
id bigint unsigned not null auto_increment,
nombre varchar(255) not null,
direccion varchar(255) not null,
correo_electronico varchar(255) not null,
primary key(id)
);
Conexión
Aquí asumo que ya leíste los tutoriales de MySQL que dejé arriba, y que también sabes tu contraseña, usuario y esas cosas. La conexión es así:
package main
import (
"database/sql" // Interactuar con bases de datos
"fmt" // Imprimir mensajes y esas cosas
_ "github.com/go-sql-driver/mysql" // La librería que nos permite conectar a MySQL
)
func obtenerBaseDeDatos() (db *sql.DB, e error) {
usuario := "root"
pass := ""
host := "tcp(127.0.0.1:3306)"
nombreBaseDeDatos := "agenda"
// Debe tener la forma usuario:contraseña@host/nombreBaseDeDatos
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@%s/%s", usuario, pass, host, nombreBaseDeDatos))
if err != nil {
return nil, err
}
return db, nil
}
func main() {
db, err := obtenerBaseDeDatos()
if err != nil {
fmt.Printf("Error obteniendo base de datos: %v", err)
return
}
// Terminar conexión al terminar función
defer db.Close()
// Ahora vemos si tenemos conexión
err = db.Ping()
if err != nil {
fmt.Printf("Error conectando: %v", err)
return
}
// Listo, aquí ya podemos usar a db!
fmt.Printf("Conectado correctamente")
}
La parte más importante es en donde hacemos la conexión. Ahí especificamos la contraseña, el host, el usuario y el nombre de la base de datos. El host siempre es localhost, pero puede que en tu caso sea otro (qué tal si es un servidor remoto), igualmente puedes cambiar el puerto.
Los errores más comunes son los siguientes:
Cuando especificamos mal la IP o el puerto: Error conectando: dial tcp 127.0.0.1:3306: connectex: No se puede establecer una conexión ya que el equipo de destino denegó expresamente dicha conexión.
Si no ponemos el usuario o la contraseña correcta: Error conectando: Error 1044: Access denied for user ”@’localhost’ to database ‘agenda’
En caso de que no exista la base de datos: Error conectando: Error 1049: Unknown database ‘agenda’
Esta función la vamos a estar llamando desde las otras funciones del CRUD, así podemos obtener la base de datos desde cualquier lugar. No olvides manejar propiamente la base de datos, por ejemplo cerrarla al final con db.Close()
y comprobar siempre si err
no es nulo con if err != nil
.
Nota: el ping no siempre es necesario, ya que igualmente si queremos hacer una consulta y algo anda mal, se nos notificará. Digo esto para evitar confusiones más tarde.
Abstraer todo en una clase
Yo sé que esto no es una clase, porque para empezar esto no es Java (qué alivio), pero vamos a poner todo en algo que yo llamo clase, lo que en realidad es un tipo struct que definimos nosotros. Esto representará a nuestro contacto:
type Contacto struct {
Nombre, Direccion, CorreoElectronico string
Id int
}
Tiene todo lo que tiene nuestra base de datos. Este struct nos lo estaremos pasando para hacer todas las operaciones.
Insertar datos a MySQL con Go
Ahora veamos la operación de insertar o crear datos. Para ello creamos esta función:
func main() {
c := Contacto{
Nombre: "Luis Cabrera Benito",
Direccion: "Calle Sin Nombre #123",
CorreoElectronico: "contacto@parzibyte.me",
}
err := insertar(c)
if err != nil {
fmt.Printf("Error insertando: %v", err)
}else{
fmt.Println("Insertado correctamente")
}
}
func insertar(c Contacto) (e error) {
db, err := obtenerBaseDeDatos()
if err != nil {
return err
}
defer db.Close()
// Preparamos para prevenir inyecciones SQL
sentenciaPreparada, err := db.Prepare("INSERT INTO agenda (nombre, direccion, correo_electronico) VALUES(?, ?, ?)")
if err != nil {
return err
}
defer sentenciaPreparada.Close()
// Ejecutar sentencia, un valor por cada '?'
_, err = sentenciaPreparada.Exec(c.Nombre, c.Direccion, c.CorreoElectronico)
if err != nil {
return err
}
return nil
}
La función recibe un struct que define un Contacto. Regresa un error. Siempre debemos comprobar si los errores son nulos.
En caso de que no haya error, regresará nil
. Estamos previniendo inyecciones SQL porque preparamos la sentencia antes de ejecutarla; es decir, no concatenamos.
También estamos reutilizando la función obtenerBaseDeDatos
.
Leer datos de una tabla en MySQL con Go
Toca el turno de la operación select. Ahora leeremos todos los datos y regresamos un arreglo con los mismos. No recomiendo hacer esto en la vida real (¿por qué alguien iba a querer todos los datos en un arreglo?), y si lo hacemos, recuerda hacerlo con arreglos pequeños.
En este caso regresamos todos los existentes porque es algo educativo. La función queda así:
func obtenerContactos() ([]Contacto, error) {
contactos := []Contacto{}
db, err := obtenerBaseDeDatos()
if err != nil {
return nil, err
}
defer db.Close()
filas, err := db.Query("SELECT id, nombre, direccion, correo_electronico FROM agenda")
if err != nil {
return nil, err
}
// Si llegamos aquí, significa que no ocurrió ningún error
defer filas.Close()
// Aquí vamos a "mapear" lo que traiga la consulta en el while de más abajo
var c Contacto
// Recorrer todas las filas, en un "while"
for filas.Next() {
err = filas.Scan(&c.Id, &c.Nombre, &c.Direccion, &c.CorreoElectronico)
// Al escanear puede haber un error
if err != nil {
return nil, err
}
// Y si no, entonces agregamos lo leído al arreglo
contactos = append(contactos, c)
}
// Vacío o no, regresamos el arreglo de contactos
return contactos, nil
}
Regresa un arreglo de structs de tipo Contacto. ¿Ya vieron cómo está sirviendo el struct que creamos hace un momento? igualmente, como segunda respuesta regresa un error que siempre debemos revisar.
Para iterarlo podemos usar un range
en un for
, así:
contactos, err := obtenerContactos()
if err != nil {
fmt.Printf("Error obteniendo contactos: %v", err)
return
}
for _, contacto := range contactos {
fmt.Printf("%v\n", contacto)
}
De esta manera se imprime el struct, ya con eso podemos ver cómo podemos leer datos de MySQL usando Go.
Actualizar datos de una tabla en MySQL con Golang
El tercer pilar de un CRUD es Update. Veamos cómo actualizar un datos de una tabla en MySQL usando Go. En este caso, crearemos una función que recibe un struct de tipo contacto, del que leerá el id y a partir de él actualizará. Queda así:
func actualizar(c Contacto) error {
db, err := obtenerBaseDeDatos()
if err != nil {
return err
}
defer db.Close()
sentenciaPreparada, err := db.Prepare("UPDATE agenda SET nombre = ?, direccion = ?, correo_electronico = ? WHERE id = ?")
if err != nil {
return err
}
defer sentenciaPreparada.Close()
// Pasar argumentos en el mismo orden que la consulta
_, err = sentenciaPreparada.Exec(c.Nombre, c.Direccion, c.CorreoElectronico, c.Id)
return err // Ya sea nil o sea un error, lo manejaremos desde donde hacemos la llamada
}
Recibe, de nuevo, un struct de tipo Contacto. Y devuelve un error que puede ser nulo. Lo importante aquí es la consulta.
Cabe mencionar que no necesariamente debemos actualizar todos los campos, pero lo hacemos para ejemplificar todo lo que se puede alcanzar. Para llamarlo podemos hacer esto:
contactoNuevo := Contacto{
Id: 1, // El id del contacto que vamos a actualizar
Nombre: "Este es el nuevo nombre",
Direccion: "Nueva dirección",
CorreoElectronico: "parzibyte@gmail.com",
}
err := actualizar(contactoNuevo)
if err != nil {
fmt.Printf("Error actualizando: %v", err)
} else {
fmt.Println("Actualizado correctamente")
}
Ese struct puede ser creado por nosotros, o a través del usuario.
Eliminar datos de MySQL o MariaDB desde Go
El último paso del CRUD es DELETE. Vamos a ver cómo eliminar un registro a través de su id. La función ahora queda así:
func eliminar(c Contacto) error {
db, err := obtenerBaseDeDatos()
if err != nil {
return err
}
defer db.Close()
sentenciaPreparada, err := db.Prepare("DELETE FROM agenda WHERE id = ?")
if err != nil {
return err
}
defer sentenciaPreparada.Close()
_, err = sentenciaPreparada.Exec(c.Id)
if err != nil {
return err
}
return nil
}
Aquí puede que nos preguntemos, ¿por qué no recibir un id entero? y la respuesta es que depende de nosotros. Yo lo dejo así por si en un futuro necesito saber más cosas de Contacto.
¿Qué tal si necesitamos loguear el nombre del contacto? o tal vez enviarle un correo antes de eliminarlo, cosas de esas.
Para implementar la eliminación, podemos usar este fragmento. En ese caso sólo proporcionamos el Id, pero podemos llenar los más datos.
contactoParaEliminar := Contacto{
Id: 1,
}
err := eliminar(contactoParaEliminar)
if err != nil {
fmt.Printf("Error al eliminar: %v", err)
} else {
fmt.Println("Eliminado correctamente")
}
Así es como terminamos este CRUD de MySQL + Go. Es hora de poner todo junto.
Poniendo todo junto
Como esta app se ejecutará en consola vamos a hacer un pequeño menú y dar opciones al usuario.
func main() {
creditos := `==========================================================
CRUD de MySQL y GO
__ __ __
.-----.---.-.----.-----|__| |--.--.--| |_.-----.
| _ | _ | _|-- __| | _ | | | _| -__|
| __|___._|__| |_____|__|_____|___ |____|_____|
|__| |_____|
==========================================================`
fmt.Println(creditos)
menu := `¿Qué deseas hacer?
[1] -- Insertar
[2] -- Mostrar
[3] -- Actualizar
[4] -- Eliminar
[5] -- Salir
-----> `
var eleccion int
var c Contacto
for eleccion != 5 {
fmt.Print(menu)
fmt.Scanln(&eleccion)
scanner := bufio.NewScanner(os.Stdin)
switch eleccion {
case 1:
fmt.Println("Ingresa el nombre:")
if scanner.Scan() {
c.Nombre = scanner.Text()
}
fmt.Println("Ingresa la dirección:")
if scanner.Scan() {
c.Direccion = scanner.Text()
}
fmt.Println("Ingresa el correo electrónico:")
if scanner.Scan() {
c.CorreoElectronico = scanner.Text()
}
err := insertar(c)
if err != nil {
fmt.Printf("Error insertando: %v", err)
} else {
fmt.Println("Insertado correctamente")
}
case 2:
contactos, err := obtenerContactos()
if err != nil {
fmt.Printf("Error obteniendo contactos: %v", err)
} else {
for _, contacto := range contactos {
fmt.Println("====================")
fmt.Printf("Id: %d\n", contacto.Id)
fmt.Printf("Nombre: %s\n", contacto.Nombre)
fmt.Printf("Dirección: %s\n", contacto.Direccion)
fmt.Printf("E-mail: %s\n", contacto.CorreoElectronico)
}
}
case 3:
fmt.Println("Ingresa el id:")
fmt.Scanln(&c.Id)
fmt.Println("Ingresa el nuevo nombre:")
if scanner.Scan() {
c.Nombre = scanner.Text()
}
fmt.Println("Ingresa la nueva dirección:")
if scanner.Scan() {
c.Direccion = scanner.Text()
}
fmt.Println("Ingresa el nuevo correo electrónico:")
if scanner.Scan() {
c.CorreoElectronico = scanner.Text()
}
err := actualizar(c)
if err != nil {
fmt.Printf("Error actualizando: %v", err)
} else {
fmt.Println("Actualizado correctamente")
}
case 4:
fmt.Println("Ingresa el ID del contacto que deseas eliminar:")
fmt.Scanln(&c.Id)
err := eliminar(c)
if err != nil {
fmt.Printf("Error eliminando: %v", err)
} else {
fmt.Println("Eliminado correctamente")
}
}
}
}
Un sencillo ciclo While en donde implementamos la lectura de datos por teclado V1 y también la versión 2 para leer la elección del usuario. La diferencia entre las mismas es que la primera lee toda la línea, y la segunda lee hasta encontrar un espacio en blanco.
Código final: CRUD en Go y MySQL
Finalmente, si te da pereza juntar todo, aquí dejo el código completo:
/*
Un CRUD completo de GoLang y MySQL
@author parzibyte
*/
package main
import (
"bufio" // Leer líneas incluso si tienen espacios
"database/sql" // Interactuar con bases de datos
"fmt" // Imprimir mensajes y esas cosas
_ "github.com/go-sql-driver/mysql" // La librería que nos permite conectar a MySQL
"os" // El búfer, para leer desde la terminal con os.Stdin
)
type Contacto struct {
Nombre, Direccion, CorreoElectronico string
Id int
}
func obtenerBaseDeDatos() (db *sql.DB, e error) {
usuario := "root"
pass := ""
host := "tcp(127.0.0.1:3306)"
nombreBaseDeDatos := "agenda"
// Debe tener la forma usuario:contraseña@protocolo(host:puerto)/nombreBaseDeDatos
db, err := sql.Open("mysql", fmt.Sprintf("%s:%s@%s/%s", usuario, pass, host, nombreBaseDeDatos))
if err != nil {
return nil, err
}
return db, nil
}
func main() {
creditos := `==========================================================
CRUD de MySQL y GO
__ __ __
.-----.---.-.----.-----|__| |--.--.--| |_.-----.
| _ | _ | _|-- __| | _ | | | _| -__|
| __|___._|__| |_____|__|_____|___ |____|_____|
|__| |_____|
==========================================================`
fmt.Println(creditos)
menu := `¿Qué deseas hacer?
[1] -- Insertar
[2] -- Mostrar
[3] -- Actualizar
[4] -- Eliminar
[5] -- Salir
-----> `
var eleccion int
var c Contacto
for eleccion != 5 {
fmt.Print(menu)
fmt.Scanln(&eleccion)
scanner := bufio.NewScanner(os.Stdin)
switch eleccion {
case 1:
fmt.Println("Ingresa el nombre:")
if scanner.Scan() {
c.Nombre = scanner.Text()
}
fmt.Println("Ingresa la dirección:")
if scanner.Scan() {
c.Direccion = scanner.Text()
}
fmt.Println("Ingresa el correo electrónico:")
if scanner.Scan() {
c.CorreoElectronico = scanner.Text()
}
err := insertar(c)
if err != nil {
fmt.Printf("Error insertando: %v", err)
} else {
fmt.Println("Insertado correctamente")
}
case 2:
contactos, err := obtenerContactos()
if err != nil {
fmt.Printf("Error obteniendo contactos: %v", err)
} else {
for _, contacto := range contactos {
fmt.Println("====================")
fmt.Printf("Id: %d\n", contacto.Id)
fmt.Printf("Nombre: %s\n", contacto.Nombre)
fmt.Printf("Dirección: %s\n", contacto.Direccion)
fmt.Printf("E-mail: %s\n", contacto.CorreoElectronico)
}
}
case 3:
fmt.Println("Ingresa el id:")
fmt.Scanln(&c.Id)
fmt.Println("Ingresa el nuevo nombre:")
if scanner.Scan() {
c.Nombre = scanner.Text()
}
fmt.Println("Ingresa la nueva dirección:")
if scanner.Scan() {
c.Direccion = scanner.Text()
}
fmt.Println("Ingresa el nuevo correo electrónico:")
if scanner.Scan() {
c.CorreoElectronico = scanner.Text()
}
err := actualizar(c)
if err != nil {
fmt.Printf("Error actualizando: %v", err)
} else {
fmt.Println("Actualizado correctamente")
}
case 4:
fmt.Println("Ingresa el ID del contacto que deseas eliminar:")
fmt.Scanln(&c.Id)
err := eliminar(c)
if err != nil {
fmt.Printf("Error eliminando: %v", err)
} else {
fmt.Println("Eliminado correctamente")
}
}
}
}
func eliminar(c Contacto) error {
db, err := obtenerBaseDeDatos()
if err != nil {
return err
}
defer db.Close()
sentenciaPreparada, err := db.Prepare("DELETE FROM agenda WHERE id = ?")
if err != nil {
return err
}
defer sentenciaPreparada.Close()
_, err = sentenciaPreparada.Exec(c.Id)
if err != nil {
return err
}
return nil
}
func insertar(c Contacto) (e error) {
db, err := obtenerBaseDeDatos()
if err != nil {
return err
}
defer db.Close()
// Preparamos para prevenir inyecciones SQL
sentenciaPreparada, err := db.Prepare("INSERT INTO agenda (nombre, direccion, correo_electronico) VALUES(?, ?, ?)")
if err != nil {
return err
}
defer sentenciaPreparada.Close()
// Ejecutar sentencia, un valor por cada '?'
_, err = sentenciaPreparada.Exec(c.Nombre, c.Direccion, c.CorreoElectronico)
if err != nil {
return err
}
return nil
}
func obtenerContactos() ([]Contacto, error) {
contactos := []Contacto{}
db, err := obtenerBaseDeDatos()
if err != nil {
return nil, err
}
defer db.Close()
filas, err := db.Query("SELECT id, nombre, direccion, correo_electronico FROM agenda")
if err != nil {
return nil, err
}
// Si llegamos aquí, significa que no ocurrió ningún error
defer filas.Close()
// Aquí vamos a "mapear" lo que traiga la consulta en el while de más abajo
var c Contacto
// Recorrer todas las filas, en un "while"
for filas.Next() {
err = filas.Scan(&c.Id, &c.Nombre, &c.Direccion, &c.CorreoElectronico)
// Al escanear puede haber un error
if err != nil {
return nil, err
}
// Y si no, entonces agregamos lo leído al arreglo
contactos = append(contactos, c)
}
// Vacío o no, regresamos el arreglo de contactos
return contactos, nil
}
func actualizar(c Contacto) error {
db, err := obtenerBaseDeDatos()
if err != nil {
return err
}
defer db.Close()
sentenciaPreparada, err := db.Prepare("UPDATE agenda SET nombre = ?, direccion = ?, correo_electronico = ? WHERE id = ?")
if err != nil {
return err
}
defer sentenciaPreparada.Close()
// Pasar argumentos en el mismo orden que la consulta
_, err = sentenciaPreparada.Exec(c.Nombre, c.Direccion, c.CorreoElectronico, c.Id)
return err // Ya sea nil o sea un error, lo manejaremos desde donde hacemos la llamada
}
Así es como terminamos. Fue un tutorial extenso pero cumple con su propósito. No te quedes con esto, te invito a leer más ejemplos en la Wiki de la librería.
Más tutoriales de: MySQL | Bases de Datos | Go
Hola amigo muchas gracias por la guía, esta muy buena.
Tengo un problema, al momento de insertar datos estos se replican en la base de datos, por lo que se guardan 2 veces.
Sabes por que me sucede esto?
Hola, ya lo resolví, resulta que mi antivirus estaba provocando que replicaran los datos. Intente agregar el archivo a las excepciones pero igual lo sigue haciendo, así que me tocó desactivarlo y ahora si funciona perfecto.
Me parece perfecto. No olvides seguirme y compartir 🙂
Saludos
Excelente tutorial, te agradezco el tiempo que te tomaste para hacer que otros aprendan desde cero este gran lenguaje.
Saludos.
Gracias por tus comentarios 🙂 más tutoriales de Go vienen en camino, te invito a suscribirte a mi blog y a seguirme en mis redes sociales.
Saludos
Execlente tutorial ! . de verdad el mejor , felicitaciones, simple y bien explicado !