Tipos de datos, estructuras y comportamientos en Go

Una de las cosas que más me ha agradado en mi aprendizaje de Go ha sido poder definir comportamientos específicos para los tipos y estructuras de datos de forma sencilla. Por ejemplo cuando definimos una estructura en nuestro módulo y deseamos darle un formato específico al imprimirlo en pantalla.

Para observar esta definición de comportamientos analizaremos una pequeña herramienta que convierte entre grados Celsius (centígrados), Farenheit y Kelvin.

El código lo pueden encontrar en Gitlab.

Definiendo los tipos y estructuras de datos

Comenzamos declarando los tipos de datos que usaremos para representar las unidades de temperatura.

**type** Farenheit float64
**type** Celsius float64
**type** Kelvin float64

Cada tipo se define como un alias del tipo primitivo float64, que representa a un número decimal de doble precisón.

Ahora, con el ánimo de ampliar el ejercicio, agregaremos una estructura de datos que nos permitirá encapsular mediciones equivalentes en las tres unidades de temperatura.

type Equivalents struct {
	f Farenheit
	c Celsius
	k Kelvin
}

Esta estructura nos ayudará a realizar la conversión de una unidad a sus dos equivalentes mediante una función específica.

Definiendo el comportamiento

Para definir una función de comportamiento asociada a un tipo de datos Type en Go se utiliza la siguiente sintaxis.

func (t Type) FunctionName() ReturnType {}

Donde ReturnType se especifica sólo en caso de que la función retorne datos.

Conversiones entre las diferentes unidades

La principal utilidad de esta pequeña librería es poder convertir entre las tres diferentes unidades de temperatura. Los cálculos que hay que realizar los he obtenido de esta página y los he trasladado a Go.

Por ejemplo dados los grados centígrados queremos saber a cuánto equivale en Farenheit y Kelvin. Esto lo podemos realizar de la siguiente manera.

func (c Celsius) IntoFarenheit() Farenheit {
	return Farenheit( c * 1.8 + 32 )
}

func (c Celsius) IntoKelvin() Kelvin {
	return Kelvin(c + 273.15)
}

Algo que vale la pena mencionar es que para manejar correctamente los tipos de datos debemos convertir los valores encapsulándolos con el tipo como si se tratase de una función: **Farenheit( c * 1.8 + 32 )**.

Ahora para definir cómo se comportan las conversiones desde Farenheit y Kelvin

func (k Kelvin) IntoFarenheit() Farenheit {
	return Farenheit((k - 273.15) * 1.8 + 32)
}

func (k Kelvin) IntoCelsius() Celsius {
	return Celsius(k - 273.15)
}

func (f Farenheit) IntoCelsius() Celsius {
	return Celsius( (f - 32) / 1.8 )
}

func (f Farenheit) IntoKelvin() Kelvin {
	return Kelvin( (f - 32) / 1.8 + 273.15 )
}

Observen que hemos definido las funciones IntoCelsius(), IntoKelvin() e IntoFarenheit() en múltiples ocasiones dependiendo del tipo de datos que requiere la conversión.

Finalmente haremos uso de nuestra estructura que encapsula equivalentes. Para construirla utilizaremos nuestras conversiones ya preparadas.

func (c Celsius) CalculateEquivalents() Equivalents {
	return Equivalents{ f: c.IntoFarenheit(), c: c, k: c.IntoKelvin() }
}

func (k Kelvin) CalculateEquivalents() Equivalents {
	return Equivalents{ f: k.IntoFarenheit(), c: k.IntoCelsius(), k: k }
}

func (f Farenheit) CalculateEquivalents() Equivalents {
	return Equivalents{ f: f, c: f.IntoCelsius(), k: f.IntoKelvin() }
}

Representación Textual

Ahora definiremos cómo queremos que nuestras unidades de temperatura sean representadas en texto.

func (c Celsius) String() string {
	return fmt.Sprintf("%.2f °C", c)
}

func (c Farenheit) String() string {
	return fmt.Sprintf("%.2f °F", c)
}

func (k Kelvin) String() string {
	return fmt.Sprintf("%.2f °K", k)
}

Para cada una de las unidades estamos imprimiendo el número en float64 usando sólo dos decimales: %.2f y adjuntamos la letra asociada a cada unidad.

En este caso estamos implementando el método String() de la interfaz Stringer que es parte del paquete fmt. Stringer es una de las interfaces más utilizadas en Go. Al implementarla le damos la capacidad a nuestro tipo de datos de declarar cómo será su representación en texto.

En el caso de nuestras mediciones equivalentes he decidido que en formato texto serán representadas de la siguiente manera

func (e Equivalents) String() string {
	return fmt.Sprintf("Estas unidades son equivalentes => %s - %s - %s", e.c, e.f, e.k)
}

Utilizando la librería

Ahora probaremos nuestras conversiones en una pequeña función.

func main() {
	fmt.Println("**Conversiones**")
	bodyTemperature := conversions.Celsius(37)

	fmt.Println("La temperatura corporal promedio es", bodyTemperature)

	equiv := bodyTemperature.CalculateEquivalents()

	fmt.Println(equiv)
}

Al ejecutar este código obtenemos

; go run .
**Conversiones**
La temperatura corporal promedio es 37.00 °C
Estas unidades son equivalentes => 37.00 °C - 98.60 °F - 310.15 °K

En Go podemos asociar comportamientos a nuestros tipos y estructuras de datos de forma sencilla. En este artículo hemos visto la forma simple de realizarlo. Con ellos otorgamos polimorfismo a nuestros módulos.

Go cuenta también con [interfaces](https://go.dev/tour/methods/9) que tienen la característica de predefinir las funciones y sus encabezados y que formaliza el uso de comportamientos como los que acabamos de observar.