Nivel 3: Creemos un Gestor de Tareas en Golang
Nivel 3: Creemos un Gestor de Tareas en Go
¡Hola, Gophers! 👋 Bienvenidos a nuestro tercer post de la serie de fundamentos de Go. Hoy vamos a subir de nivel y crear algo que realmente puedes usar en tu día a día: un Gestor de Tareas en línea de comandos. ¿Estás listo para organizar tu vida y aprender Go al mismo tiempo? ¡Go!
🎯 ¿Qué vamos a construir?
Vamos a crear un programa que te permita:
- Añadir nuevas tareas
- Listar tus tareas
- Marcar tareas como completadas
- Guardar tus tareas en un archivo
- Cargar tus tareas desde un archivo
- Crear ejecutable para cualquier sistema operativo
Y mientras lo hacemos, aprenderemos sobre estructuras, métodos, slices, manejo de archivos y más.
🧱 Paso 1: Definiendo nuestra Estructura de Tarea
En Go, usamos estructuras (structs) para crear nuestros propios tipos de datos. Vamos a definir una estructura para nuestras tareas:
package main import ( "time" ) type Task struct { ID int Title string Description string Done bool DueDate time.Time }
¿Ves cómo cada tarea tiene un ID, un título, una descripción, un estado de completado y una fecha de vencimiento? Esto es como crear un molde para nuestras tareas.
🛠 Paso 2: Añadiendo "Métodos" a nuestra Estructura
Los métodos son funciones asociadas a una estructura, está entre comillas la palabra método porque éste es un concepto más de objetos, lo cual Go no tiene. Realmente la palabra más adecuada en el contexto de Go es receiver. Vamos a añadir un receiver para marcar una tarea como completada:
func (t *Task) MarkAsDone() { t.Done = true }
Este método pertenece a la estructura Task
. El *
antes de Task
significa que estamos modificando la tarea original, no una copia.
📋 Paso 3: Funciones para Manejar Tareas
Antes de sumergirnos en las funciones para manejar tareas, vamos a entender mejor algunos conceptos fundamentales de Go: arrays, slices y la función append
.
Arrays vs Slices en Go
En Go, tenemos dos formas principales de manejar colecciones de elementos: arrays y slices.
Arrays
Un array en Go tiene un tamaño fijo y se declara así:
var miArray [5]int
Esto crea un array de 5 enteros. Los arrays son útiles cuando sabes exactamente cuántos elementos necesitas, pero son inflexibles porque su tamaño no puede cambiar.
Slices
Los slices, por otro lado, son más flexibles. Un slice es como una vista dinámica de un array. Se declara así:
var miSlice []int
Los slices no tienen un tamaño fijo y pueden crecer o encogerse según sea necesario. Nota que simplemente no le decimos un valor dentro de [ ]
La función append
append
es una función incorporada en Go que se usa para añadir elementos a un slice. Funciona así:
miSlice = append(miSlice, 1, 2, 3)
Esto añade los elementos 1, 2 y 3 al final de miSlice
. Lo interesante de append
es que si el slice no tiene suficiente capacidad, automáticamente crea un nuevo array subyacente más grande y copia los elementos.
Aplicando estos conceptos a nuestro Gestor de Tareas
Ahora que entendemos estos conceptos, vamos a ver cómo los aplicamos en nuestras funciones para manejar tareas:
func AddTask(tasks []Task, newTask Task) []Task { return append(tasks, newTask) } func ListTasks(tasks []Task) { for _, task := range tasks { status := "Pendiente" if task.Done { status = "Completada" } fmt.Printf("[%d] %s - %s (Vence: %s) - %s\n", task.ID, task.Title, task.Description, task.DueDate.Format("02/01/2006"), status) } }
Analicemos estas funciones:
- AddTask:
- Toma un slice de
Task
y una nuevaTask
como argumentos. - Usa
append
para añadir la nueva tarea al slice. - Retorna el slice actualizado.
- Toma un slice de
- ListTasks:
- Toma un slice de
Task
como argumento. - Usa un bucle
for
conrange
para iterar sobre el slice. for _, task := range tasks
es una forma concisa de Go para decir "para cada tarea en el slice de tareas".- El
_
se usa para ignorar el índice querange
normalmente proporcionaría, ya que no lo necesitamos aquí.
Ejemplo práctico
Veamos cómo podríamos usar estas funciones:
func main() { var tasks []Task // Inicializamos un slice vacío // Añadimos algunas tareas tasks = AddTask(tasks, Task{ID: 1, Title: "Comprar Vegetales", Description: "Lechuga, Espinaca"}) tasks = AddTask(tasks, Task{ID: 2, Title: "Hacer ejercicio", Description: "30 minutos de cardio"}) // Listamos las tareas ListTasks(tasks) }
Este código crearía un slice vacío, añadiría dos tareas y luego las listaría.
Casi siempre utilizarás más slices que arrays, porque tiene:
- Flexibilidad: Los slices pueden crecer según sea necesario, perfecto para una lista de tareas que cambia constantemente.
- Facilidad de uso: Funciones como
append
hacen que trabajar con slices sea muy cómodo. - Eficiencia: Aunque los slices usan arrays por debajo, manejan automáticamente la creación de nuevos arrays más grandes cuando es necesario, sin que tengamos que preocuparnos por ello.
💾 Paso 4: Guardando y Cargando Tareas
En este paso, vamos a aprender cómo guardar nuestras tareas en un archivo y cargarlas de nuevo. Esto introduce varios conceptos nuevos y emocionantes en Go: manejo de archivos, serialización JSON, el uso de defer
, y manejo de errores más avanzado.
Conceptos nuevos:
- Manejo de archivos: Cómo crear, escribir y leer archivos en Go.
- JSON: Un formato ligero de intercambio de datos que usaremos para guardar nuestras tareas.
- defer: Una palabra clave en Go para programar la ejecución de una función.
- Codificación y decodificación JSON: Cómo convertir nuestras estructuras Go a JSON y viceversa.
Veamos el código y luego lo desglosaremos:
import ( "encoding/json" "os" ) func SaveTasks(tasks []Task) error { file, err := os.Create("tasks.json") if err != nil { return err } defer file.Close() encoder := json.NewEncoder(file) return encoder.Encode(tasks) } func LoadTasks() ([]Task, error) { file, err := os.Open("tasks.json") if err != nil { return nil, err } defer file.Close() var tasks []Task decoder := json.NewDecoder(file) err = decoder.Decode(&tasks) return tasks, err }
Desglosando el código:
1. Manejo de archivos
Go proporciona el paquete os
para operaciones de sistema, incluyendo el manejo de archivos.
os.Create("tasks.json")
: Crea un nuevo archivo llamado "tasks.json". Si el archivo ya existe, lo sobrescribe.os.Open("tasks.json")
: Abre un archivo existente para lectura.
Ambas funciones devuelven un *File
y un error
. En Go, es común que las funciones devuelvan un valor y un error, permitiendo un manejo de errores más explícito.
2. defer
defer file.Close()
es una instrucción muy útil en Go. Programa el cierre del archivo para que ocurra justo antes de que la función termine, sin importar cómo termine (éxito o error). Esto nos asegura que siempre cerramos nuestros archivos, evitando fugas de recursos.
3. Serialización JSON
JSON (JavaScript Object Notation) es un formato ligero de intercambio de datos. Go tiene excelente soporte para JSON en su biblioteca estándar con el paquete encoding/json
.
json.NewEncoder(file)
: Crea un nuevo codificador JSON que escribirá en el archivo.encoder.Encode(tasks)
: Convierte nuestro slice de tareas a JSON y lo escribe en el archivo.json.NewDecoder(file)
: Crea un nuevo decodificador JSON que leerá del archivo.decoder.Decode(&tasks)
: Lee el JSON del archivo y lo convierte de vuelta a un slice de tareas.
4. Manejo de errores
Go maneja los errores de forma explícita. Observa cómo verificamos err != nil
después de cada operación que podría fallar. Si hay un error, lo devolvemos inmediatamente.
Ejemplo práctico
Veamos cómo usar estas funciones en nuestro main()
:
func main() { // Cargar tareas al inicio tasks, err := LoadTasks() if err != nil { fmt.Println("No se pudieron cargar las tareas:", err) tasks = []Task{} // Si hay un error, empezamos con una lista vacía } // ... (código para añadir y listar tareas) ... // Guardar tareas antes de salir err = SaveTasks(tasks) if err != nil { fmt.Println("Error al guardar las tareas:", err) } }
Este código intenta cargar las tareas al inicio del programa. Si hay un error (por ejemplo, si el archivo no existe aún), comenzamos con una lista vacía. Al final del programa, guardamos las tareas.
¿Por qué usar JSON?
- Legibilidad: JSON es fácil de leer para humanos y máquinas.
- Interoperabilidad: Muchos lenguajes y sistemas pueden trabajar con JSON.
- Flexibilidad: Podemos añadir o quitar campos fácilmente sin romper la compatibilidad.
Ventajas de este enfoque
- Persistencia: Las tareas se guardan entre ejecuciones del programa.
- Seguridad: El uso de
defer
asegura que los recursos se liberen correctamente. - Robustez: El manejo explícito de errores nos permite responder adecuadamente a problemas.
🖥 Paso 5: Interfaz de Línea de Comandos
Finalmente, vamos a juntar todo en nuestra función main
:
func main() { tasks, err := LoadTasks() if err != nil { fmt.Println("No se pudieron cargar las tareas:", err) tasks = []Task{} } for { fmt.Println("\n1. Añadir tarea") fmt.Println("2. Listar tareas") fmt.Println("3. Marcar tarea como completada") fmt.Println("4. Salir") fmt.Print("Elige una opción: ") var choice int fmt.Scanln(&choice) switch choice { case 1: var title, desc string fmt.Print("Título de la tarea: ") fmt.Scanln(&title) fmt.Print("Descripción: ") fmt.Scanln(&desc) newTask := Task{ ID: len(tasks) + 1, Title: title, Description: desc, Done: false, DueDate: time.Now().AddDate(0, 0, 7), // Vence en 7 días } tasks = AddTask(tasks, newTask) case 2: ListTasks(tasks) case 3: var id int fmt.Print("ID de la tarea completada: ") fmt.Scanln(&id) for i := range tasks { if tasks[i].ID == id { tasks[i].MarkAsDone() break } } case 4: err := SaveTasks(tasks) if err != nil { fmt.Println("Error al guardar las tareas:", err) } fmt.Println("¡Hasta luego!") return default: fmt.Println("Opción no válida") } } }
🏗️ Paso Final: Compilación para Diferentes Sistemas Operativos
¡Felicidades! Has creado un gestor de tareas funcional en Go. Ahora, vamos a dar el paso final: compilar nuestra aplicación para que pueda ejecutarse en diferentes sistemas operativos. Una de las grandes ventajas de Go es su capacidad de compilación cruzada, lo que significa que puedes crear ejecutables para diferentes sistemas desde tu máquina de desarrollo.
Compilación para tu sistema operativo actual
Primero, veamos cómo compilar para el sistema operativo que estás usando actualmente:
- Abre una terminal y navega al directorio de tu proyecto.
- Ejecuta el siguiente comando:
Esto creará un ejecutable llamadogo build -o gestor-tareas
gestor-tareas
(en Windows, serágestor-tareas.exe
). - Puedes ejecutar tu aplicación con:
./gestor-tareas
Compilación cruzada para otros sistemas operativos
Go hace que la compilación cruzada sea sorprendentemente fácil. Solo necesitas configurar dos variables de entorno: GOOS
(sistema operativo objetivo) y GOARCH
(arquitectura objetivo).
Para Windows (desde Linux o macOS):
GOOS=windows GOARCH=amd64 go build -o gestor-tareas.exe
Para macOS (desde Windows o Linux):
GOOS=darwin GOARCH=amd64 go build -o gestor-tareas-mac
Para Linux (desde Windows o macOS):
GOOS=linux GOARCH=amd64 go build -o gestor-tareas-linux
Explicación de los comandos:
GOOS
: Especifica el sistema operativo objetivo (darwin para macOS, linux para Linux, windows para Windows).GOARCH
: Especifica la arquitectura objetivo (amd64 para la mayoría de los sistemas de 64 bits modernos).-o
: Especifica el nombre del archivo de salida.
Consideraciones importantes:
- Dependencias del sistema: Si tu aplicación utiliza dependencias específicas del sistema (como bibliotecas C), la compilación cruzada puede ser más complicada.
- Pruebas: Siempre es una buena idea probar tus ejecutables en el sistema operativo objetivo, si es posible.
- CGO: Si tu aplicación usa
cgo
, la compilación cruzada puede requerir pasos adicionales. En nuestro caso, no lo usamos, así que no tendremos problemas.
Ejemplo en caso de que necesitemos recapitular:
Imagina que estás desarrollando en una máquina Linux y quieres crear versiones para todos los sistemas principales:
# Para Linux go build -o gestor-tareas-linux # Para Windows GOOS=windows GOARCH=amd64 go build -o gestor-tareas.exe # Para macOS GOOS=darwin GOARCH=amd64 go build -o gestor-tareas-mac
Después de ejecutar estos comandos, tendrás tres ejecutables, uno para cada sistema operativo principal.
Source Code
https://github.com/Gognition/gognition-nivel3-gestor-tareas.git
🎓 ¿Qué hemos aprendido?
- Estructuras (structs) para crear nuestros propios tipos de datos
- Métodos para añadir comportamiento a nuestras estructuras
- Slices para manejar colecciones de datos
- Manejo básico de archivos con JSON
- Manejo de errores
- Uso del paquete
time
para fechas - Creación de una interfaz de usuario simple en línea de comandos
- Crear Build
🛠 Reto para ti
¿Qué tal si expandimos nuestro gestor de tareas? Aquí tienes algunas ideas:
- Añade una opción para eliminar tareas.
- Implementa una función de búsqueda de tareas por título.
- Agrega categorías a las tareas y permite filtrar por categoría.
- Implementa un sistema de prioridades para las tareas.
Conclusión
¡Felicidades! Has creado una aplicación útil y has aprendido muchos conceptos nuevos de Go en el proceso. Recuerda, la práctica hace al maestro, así que sigue experimentando y expandiendo este proyecto.
¿Preguntas? ¿Ideas para mejorarlo? ¡Déjalas en los comentarios abajo! Y no olvides compartir tu versión mejorada del gestor de tareas con la comunidad.
Hasta la próxima, ¡y que la fuerza de Go te acompañe! 🐹💻
Comentarios