Nivel 6: Creemos un Convertidor de Unidades en Go
Nivel 6: Creemos un Convertidor de Unidades en Go
👋 En este post nos sumergiremos en la creación de un Convertidor de Unidades en Go. Este proyecto no solo es práctico, sino que también nos permitirá explorar conceptos fundamentales como interfaces, mapas, y el manejo de errores. ¡Vamos a continuar aprendiendo haciendo!
🎯 Objetivo del Proyecto
Nuestro objetivo es crear un convertidor de unidades que pueda manejar diferentes tipos de conversiones: longitud, peso, y temperatura. Este proyecto nos ayudará a entender mejor cómo Go utiliza interfaces para crear código flexible y cómo los mapas pueden ser herramientas poderosas para almacenar datos relacionados.
🧱 Estructura del Proyecto
Comenzaremos definiendo la estructura básica de nuestro proyecto:
package main import ( "fmt" "math" "errors" ) // Interfaces y estructuras irán aquí func main() { // Código principal irá aquí }
1. Definiendo la Interfaz Converter
Primero, definamos una interfaz que todos nuestros convertidores implementarán:
type Converter interface { Convert(value float64, from, to string) (float64, error) }
Esta interfaz define un método Convert
que toma un valor, una unidad de origen y una unidad de destino, y devuelve el valor convertido y un posible error.
2. Implementando el Convertidor de Longitud
Comencemos con el convertidor de longitud:
type LengthConverter struct { factors map[string]float64 } func NewLengthConverter() *LengthConverter { return &LengthConverter{ factors: map[string]float64{ "m": 1, "cm": 100, "mm": 1000, "km": 0.001, "ft": 3.28084, "yd": 1.09361, "mi": 0.000621371, }, } } func (lc *LengthConverter) Convert(value float64, from, to string) (float64, error) { fromFactor, fromOk := lc.factors[from] toFactor, toOk := lc.factors[to] if !fromOk || !toOk { return 0, errors.New("unidad de longitud no soportada") } meters := value / fromFactor return meters * toFactor, nil }
Explicación detallada:
- Struct LengthConverter:
- Utilizamos un
map
para almacenar los factores de conversión. Los mapas en Go son estructuras de datos clave-valor, perfectas para este caso.
- Utilizamos un
- Función NewLengthConverter:
- Esta es una función "constructora" que inicializa nuestro convertidor con un mapa de factores de conversión.
- Cada factor representa cuántas unidades de esa medida hay en un metro.
- Método Convert:
- Primero, buscamos los factores de conversión en el mapa usando la sintaxis
lc.factors[from]
. - El
, ok
en la asignación es un patrón común en Go para verificar si una clave existe en el mapa. - Si alguna unidad no está soportada, devolvemos un error usando
errors.New()
. - La conversión se hace primero a metros y luego a la unidad destino.
- Primero, buscamos los factores de conversión en el mapa usando la sintaxis
3. Implementando el Convertidor de Peso
Siguiendo un patrón similar, implementemos el convertidor de peso:
type WeightConverter struct { factors map[string]float64 } func NewWeightConverter() *WeightConverter { return &WeightConverter{ factors: map[string]float64{ "kg": 1, "g": 1000, "lb": 2.20462, "oz": 35.274, "ton": 0.001, }, } } func (wc *WeightConverter) Convert(value float64, from, to string) (float64, error) { fromFactor, fromOk := wc.factors[from] toFactor, toOk := wc.factors[to] if !fromOk || !toOk { return 0, errors.New("unidad de peso no soportada") } kg := value / fromFactor return kg * toFactor, nil }
La estructura y lógica son similares al convertidor de longitud, pero con unidades de peso.
4. Implementando el Convertidor de Temperatura
El convertidor de temperatura es un poco diferente, ya que las conversiones no son lineales:
type TemperatureConverter struct{} func (tc *TemperatureConverter) Convert(value float64, from, to string) (float64, error) { var celsius float64 switch from { case "C": celsius = value case "F": celsius = (value - 32) * 5 / 9 case "K": celsius = value - 273.15 default: return 0, errors.New("unidad de temperatura de origen no soportada") } switch to { case "C": return celsius, nil case "F": return celsius*9/5 + 32, nil case "K": return celsius + 273.15, nil default: return 0, errors.New("unidad de temperatura de destino no soportada") } }
Aquí, usamos switch
statements para manejar las diferentes conversiones. Primero convertimos a Celsius, y luego a la unidad deseada.
5. Creando el Convertidor Principal
Ahora, creemos un convertidor que pueda manejar todos los tipos:
type UnitConverter struct { lengthConverter Converter weightConverter Converter temperatureConverter Converter } func NewUnitConverter() *UnitConverter { return &UnitConverter{ lengthConverter: NewLengthConverter(), weightConverter: NewWeightConverter(), temperatureConverter: &TemperatureConverter{}, } } func (uc *UnitConverter) Convert(value float64, from, to, unitType string) (float64, error) { switch unitType { case "length": return uc.lengthConverter.Convert(value, from, to) case "weight": return uc.weightConverter.Convert(value, from, to) case "temperature": return uc.temperatureConverter.Convert(value, from, to) default: return 0, errors.New("tipo de unidad no soportado") } }
Este convertidor principal utiliza la interfaz Converter
para manejar diferentes tipos de conversiones de manera uniforme.
6. Implementando la Interfaz de Usuario
La interfaz de usuario es crucial para que nuestro convertidor sea fácil de usar. Vamos a mejorarla y explicar cada parte en detalle:
func main() { converter := NewUnitConverter() for { fmt.Println("\n===== Convertidor de Unidades =====") fmt.Println("1. Longitud") fmt.Println("2. Peso") fmt.Println("3. Temperatura") fmt.Println("4. Salir") fmt.Print("Seleccione el tipo de conversión (1-4): ") choice := getUserInput() if choice == "4" { fmt.Println("¡Gracias por usar el Convertidor de Unidades! Hasta luego.") break } if choice < "1" || choice > "3" { fmt.Println("Opción no válida. Por favor, intente de nuevo.") continue } unitType := map[string]string{ "1": "length", "2": "weight", "3": "temperature", }[choice] fmt.Print("Ingrese el valor a convertir: ") value, err := getFloatInput() if err != nil { fmt.Println("Error:", err) continue } fmt.Print("Ingrese la unidad de origen: ") from := getUserInput() fmt.Print("Ingrese la unidad de destino: ") to := getUserInput() result, err := converter.Convert(value, from, to, unitType) if err != nil { fmt.Printf("Error: %v\n", err) } else { fmt.Printf("Resultado: %.2f %s = %.2f %s\n", value, from, result, to) } } } func getUserInput() string { var input string fmt.Scanln(&input) return strings.TrimSpace(input) } func getFloatInput() (float64, error) { input := getUserInput() value, err := strconv.ParseFloat(input, 64) if err != nil { return 0, errors.New("por favor, ingrese un número válido") } return value, nil }
Explicación detallada:
- Bucle principal:
- Usamos un bucle
for
infinito para mantener el programa en ejecución hasta que el usuario decida salir. - Esto permite realizar múltiples conversiones sin tener que reiniciar el programa.
- Usamos un bucle
- Menú de opciones:
- Presentamos un menú claro con las opciones numeradas.
- Usamos
fmt.Println()
para mostrar cada opción en una nueva línea, mejorando la legibilidad.
- Obtención de entrada del usuario:
- Creamos una función auxiliar
getUserInput()
para obtener y limpiar la entrada del usuario. strings.TrimSpace()
se usa para eliminar espacios en blanco al inicio y al final de la entrada.
- Creamos una función auxiliar
- Manejo de la selección del usuario:
- Verificamos si el usuario quiere salir (opción 4) antes de proceder.
- Validamos que la entrada esté en el rango correcto (1-3) antes de continuar.
- Mapeo de selección a tipo de unidad:
- Usamos un mapa para convertir la selección del usuario al tipo de unidad correspondiente.
- Esto hace el código más limpio y fácil de mantener que un switch statement.
- Obtención del valor a convertir:
- Creamos una función
getFloatInput()
para manejar la conversión de la entrada a unfloat64
. - Usamos
strconv.ParseFloat()
para la conversión, manejando posibles errores.
- Creamos una función
- Realización de la conversión:
- Llamamos al método
Convert
de nuestroUnitConverter
. - Manejamos posibles errores y mostramos el resultado formateado.
- Llamamos al método
- Formateo de la salida:
- Usamos
fmt.Printf()
con formateadores (%.2f
) para mostrar el resultado con 2 decimales.
- Usamos
- Manejo de errores:
- Mostramos mensajes de error claros cuando algo va mal, mejorando la experiencia del usuario.
- Limpieza de la entrada:
strings.TrimSpace()
asegura que espacios adicionales no causen problemas en la entrada.
🧪 Probando Nuestro Convertidor
Ahora puedes compilar y ejecutar tu programa:
go run main.go
Prueba diferentes conversiones, por ejemplo:
- 1 km a m
- 2.5 kg a lb
- 32 F a C
Source Code
🎓 Conceptos Aprendidos
- Interfaces: Usamos la interfaz
Converter
para definir un comportamiento común. - Structs y Métodos: Implementamos convertidores específicos como structs con métodos.
- Mapas: Utilizamos mapas para almacenar factores de conversión.
- Manejo de Errores: Devolvimos errores en caso de unidades no soportadas.
- Switch Statements: Los usamos para manejar diferentes tipos de conversiones.
- Paquete math: Aunque no lo usamos extensivamente en este ejemplo, es crucial para cálculos más complejos.
🚀 Retos Adicionales
Conclusión
¡Felicidades! Has creado un convertidor de unidades funcional en Go. Este proyecto te ha permitido practicar con interfaces, mapas, manejo de errores y más. Recuerda, la práctica constante es clave para dominar Go.
¿Qué otros proyectos te gustaría ver? ¿Tienes alguna pregunta sobre los conceptos que hemos cubierto? ¡Déjanos tus comentarios abajo!
Comentarios