Es hora de dar un paso más con Go y que aprendamos a utilizar struct. Los struct no son más que colecciones de campos tipadas. El ejemplo típico es que tenemos el tipo de estructura de persona tiene campos de nombre como string y edad como integer.
type person struct {
name string
age int
}
Por eso vamos a presentar un pequeño problema que iremos resolviendo haciendo test antes de tirar una linea de código de producción. TDD Style.

Supongamos que queremos implementar una calculadora geométrica, que calcule el área de un triangulo.
Escribiendo el primer test: area de un triángulo
package geometry
import "testing"
func TestArea(t *testing.T) {
got := Area(12.0, 6)
expected := 36.0
if got != expected {
t.Errorf("got %.2f expected %.2f", got, expected)
}
}
Como ya aprendimos en el tutorial anterior: hemos creado una clase de test llamada geometry_test.go, hemos creado una clase para el código y ambas están en el mismo package llamado geometry. Por último también hemos creado el primer test, que como detalle especial tiene %.2f
para indicar que vamos a imprimir un float con 2 decimales.
Ejecutamos el test y tenemos:
$ go test
# github.com/jeslopcru/golang-examples/03-geometry
./geometry_test.go:6:9: undefined: Area
Escribiendo el código
Si ejecutamos el test nos dirá que la función «area» no está definida. Creamos el fichero y definimos algo como esto:
package geometry
func Area(base float64, height float64) interface{} {
return (base * height) / 2
}
Así de fácil. Ya tenemos nuestro código funcionando.
$ go test
=== RUN TestArea
--- PASS: TestArea (0.00s)
PASS
Añadiendo la funcionalidad: calcular el perímetro
Ahora vamos a crear una función para perímetro de un triangulo. El perímetro de un triangulo es la suma de sus lados.
Ya sabemos cómo funciona TDD, así que cuando terminemos tendremos algo como esto en los tests:
func TestArea(t *testing.T) {
got := Area(12.0, 6)
expected := 36.0
if got != expected {
t.Errorf("got %.2f expected %.2f", got, expected)
}
}
func TestPerimeter(t *testing.T) {
got := Perimeter(12.0, 6, 6.0)
expected := 24.0
if got != expected {
t.Errorf("got %.2f expected %.2f", got, expected)
}
}
Al ejecutarlo dará error y ni siquiera compilará:
$ go test
# github.com/jeslopcru/golang-examples/03-geometry
./geometry_test.go:15:9: undefined: Perimeter
Con este error solo nos queda volver a verde escribiendo el código de producción tal que así:
// GIVEN base and height integers WHEN call Area function THEN result is the area of a triangle
func Area(base float64, height float64) interface{} {
return (base * height) / 2
}
// GIVEN three side of a triangle WHEN call Perimeter function THEN result is the perimeter of a triangle
func Perimeter(a float64, b float64, c float64) interface{} {
return a + b + c
}
y al ejecutar los tests…
$ go test
=== RUN TestArea
--- PASS: TestArea (0.00s)
=== RUN TestPerimeter
--- PASS: TestPerimeter (0.00s)
PASS
Poco a poco hemos escrito documentación de todas las funciones que tenemos. Como ya comentamos en el post anterior estamos documentado las funciones que hacemos para que así la página de la documentación sea más rica.
Si además necesitamos añadir algún ejemplo a la documentación, solo tenemos que crear un test con prefijo example como vimos en el primer post de la serie
Refactorizando: Dando un poco de semántica
Como hemos comprobado, nuestro código funciona y hace lo que dice. Aunque no se lee por ningún lado la palabra Triangulo.
Una solución podría ser hacer más semánticas las funciones, es decir, en vez de llamara a la función «Area» que sea algo así como «AreaTriangulo». Como la idea de estos pos es que aprendamos más sobre Go, vamos a optar por una solución más «encapsulada», crearemos nuestro propio tipo: Triangulo el cual encapsulará todo estos conceptos para nosotros.
Vamos a crear tipo simple usando struct
que será nuestra «colección» en la que vamos a guardar los datos.
type Triangle struct {
a float64
b float64
c float64
}
Ahora tenemos que utilizar este nuevo tipo en el código. Cómo estamos haciendo TDD, vamos a empezar modificando los tests. Es importante ir dando «Baby stepts»,
es decir haciendo cambios muy pequeños en el código para que no empiece a fallar todo. La idea es ir ganado pequeñas batallas. Por eso vamos a empezar a modificar solo el test de perímetro:
func TestPerimeter(t *testing.T) {
triangle := Triangle{12.0,6,6.0}
got := Perimeter(triangle)
expected := 24.0
if got != expected {
t.Errorf("got %.2f expected %.2f", got, expected)
}
}
Si ejecutamos los tests obtendremos algo como esto:
$ go test
# github.com/jeslopcru/golang-examples/03-geometry
./geometry_test.go:15:14: undefined: Triangle
./geometry_test.go:16:18: not enough arguments in call to Perimeter
Vamos a modificar el código para añadir la estructura Triangle y la función perímetro para que acepte dicha estructura.
type Triangle struct {
a float64
b float64
c float64
}
// GIVEN three side of a triangle WHEN call Perimeter function THEN result is the perimeter of a triangle
func Perimeter(aTriangle Triangle) interface{} {
return aTriangle.a + aTriangle.b + aTriangle.c
}
Con este cambio, ya podemos ejecutar nuestros tests y todo parece funcionar:
go test
=== RUN TestArea
--- PASS: TestArea (0.00s)
=== RUN TestPerimeter
--- PASS: TestPerimeter (0.00s)
PASS
Ahora vamos a por la siguiente batalla, calcular el área. Aunque antes tenemos que refrescar un poco de geometría… dados los 3 lados de un triangulo, el Área se calcula a través de la formula de Herón, no vamos a entrar en muchos detalles solo usaremos este ejemplo para los tests:
Sea un triángulo de lados conocidos, siendo estos a=4, b=5 y c=3. Su Área es 6
después de la pequeña clase de matemáticas, el test quedará algo así:
func TestArea(t *testing.T) {
triangle := Triangle{4,5,3}
got := Area(triangle)
expected := 6.00
if got != expected {
t.Errorf("got %.2f expected %.2f", got, expected)
}
}
Y al ejecutar los tests nos dirá que tenemos un error parecido al anterior:
$ go test
# github.com/jeslopcru/golang-examples/03-geometry
./geometry_test.go:7:13: not enough arguments in call to Area
have (Triangle)
want (float64, float64)
Así que ahora solo tenemos que modificar la función Area usando la formula de Heron. El fichero geometry.go quedaría así
package geometry
import "math"
type Triangle struct {
a float64
b float64
c float64
}
// GIVEN three side of a triangle WHEN call Perimeter function THEN result is the perimeter of a triangle
func Perimeter(aTriangle Triangle) interface{} {
return aTriangle.a + aTriangle.b + aTriangle.c
}
// GIVEN base and height integers WHEN call Area function THEN result is the area of a triangle
func Area(aTriangle Triangle) interface{} {
semiperimeter := (aTriangle.a + aTriangle.b + aTriangle.c) / 2
radicand := semiperimeter * (semiperimeter - aTriangle.a) * (semiperimeter - aTriangle.b) * (semiperimeter - aTriangle.c)
return math.Sqrt(radicand)
}
Y al ejecutar los tests tenemos algo así:
go test
=== RUN TestArea
--- PASS: TestArea (0.00s)
=== RUN TestPerimeter
--- PASS: TestPerimeter (0.00s)
PASS
Todo perfecto. Con esta refactorización hemos aprendido que podemos utilizar struct
para «organizar nuestros datos. Ahora sabemos como crear un struct y como utilizarlo dentro de una función (operador . para acceder a los atributos).
Del mismo modo hemos importado la librería Math para poder ejecutar la raíz cuadrada (math.Sqrt(radicand))
Ahora nuestro nuevo requisito será que hagamos el cálculo del Area para un rectángulo. Pero antes haremos un commit porque todo está en verde.
Escribiendo el test para el Area de un rectangulo
Tan solo tenemos que añadir un caso a nuestro test:
func TestArea(t *testing.T) {
t.Run("triangle", func(t *testing.T) {
triangle := Triangle{4, 5, 3}
got := Area(triangle)
expected := 6.00
if got != expected {
t.Errorf("got %.2f expected %.2f", got, expected)
}
})
t.Run("rectangle", func(t *testing.T) {
rectangle := Rectangle{12, 6}
got := Area(rectangle)
expected := 72.0
if got != expected {
t.Errorf("got %.2f expected %.2f", got, expected)
}
})
}
Y obviamente al ejecutar nos dirá que rectángulo no está definido
go test
# github.com/jeslopcru/golang-examples/03-geometry
./geometry_test.go:18:16: undefined: Rectangle
Creando el código para pasar el test
Lo primero que haremos será crear el struct
Rectangle
type Rectangle struct {
a float64
b float64
}
Si volvemos a ejecutar los test tendremos algo como esto:
go test`
# github.com/jeslopcru/golang-examples/03-geometry
./geometry_test.go:19:14: cannot use rectangle (type Rectangle) as type Triangle in argument to Area
En lenguajes como Java podríamos declarar otra función Area a que reciba como parámetro un Rectangle pero en Go… no podemos tener dos funciones con el mismo nombre.
go test
# github.com/jeslopcru/golang-examples/03-geometry
./geometry.go:31:33: Area redeclared in this block
previous declaration at ./geometry.go:23:31
Tenemos dos opciones, crear un package nuevo para así podamos tener una función con el mismo nombre o definir methods en nuestros «tipos»
Methods o métodos
Hasta ahora solo hemos escrito funciones dentro del package y aunque no lo parezca hemos usado un method.
En los tests cuando llamamos a t.Errorf...
estamos llamando al method Errorf.
Al fin y al cabo un method no es más que una función que está vinculada a algo (un receptor). Básicamente con el método asociamos una función a un tipo concreto.
Simplificando mucho, lo que vamos a hacer es crear una función dentro del struct. dicha función solo puede ser llamada por «objetos» (notemos las comillas) de ese tipo que hemos definido en el struct
.
Así que lo primero será que escribamos los tests correctamente llamando a los methods así:
func TestArea(t *testing.T) {
t.Run("triangle", func(t *testing.T) {
triangle := Triangle{4, 5, 3}
got := triangle.Area()
expected := 6.00
if got != expected {
t.Errorf("got %.2f expected %.2f", got, expected)
}
})
t.Run("rectangle", func(t *testing.T) {
rectangle := Rectangle{12, 6}
got := rectangle.Area()
expected := 72.0
if got != expected {
t.Errorf("got %.2f expected %.2f", got, expected)
}
})
}
AL ejecutar los tests nos dice que «los method Area no están definidos»
$ go test
# github.com/jeslopcru/golang-examples/03-geometry
./geometry_test.go:9:18: triangle.Area undefined (type Triangle has no field or method Area)
./geometry_test.go:19:19: rectangle.Area undefined (type Rectangle has no field or method Area)
Ahora tenemos que añadir esos method dentro de los struct
func (triangle Triangle) Area() interface{} {
semiperimeter := (triangle.a + triangle.b + triangle.c) / 2
radicand := semiperimeter * (semiperimeter - triangle.a) * (semiperimeter - triangle.b) * (semiperimeter - triangle.c)
return math.Sqrt(radicand)
}
type Rectangle struct {
a float64
b float64
}
func (rectangle Rectangle) Area() interface{} {
return rectangle.a * rectangle.b
}
La sintaxis para declarar métodos es casi la misma que cuando declaramos funciones, la única diferencia es que en vez de usar this
o similar para acceder a los elementos, utilizamos el nombre del receptor.
Es una buena práctica que hagamos que el nombre de la variable del receptor empiece por la misma letra del tipo que hemos definido.
Por ello hemos cambiado el nombre de aTriangle a triangle y los mismo con aRectangle a rectangle
Al ejecutar los test tenemos:
$ go test
=== RUN TestArea
--- PASS: TestArea (0.00s)
=== RUN TestArea/triangle
--- PASS: TestArea/triangle (0.00s)
=== RUN TestArea/rectangle
--- PASS: TestArea/rectangle (0.00s)
=== RUN TestPerimeter
--- PASS: TestPerimeter (0.00s)
PASS
Todo funciona y ya podemos eliminar nuestra antigua función Area que Golang incluso la pone en color gris para indicarnos que no se utiliza.
Solo nos queda hacer un commit y listo.
Conclusiones
Hoy hemos aprendido a importar librerías (math) para poder utilizar funciones de otros paquetes. Del mismo modo ahora sabemos definir nuestros propios tipos con struct
y además somos capaces de crear _methods` para los tipos. Con lo que tenemos un código mucho más estructurado.
Nuestro código es más semántico, está mas organizado utilizando struct y no hemos dejado de practicar TDD ni un solo momento.
¿Nos atrevemos a crear solos el área para un Circulo? ¿y que pasa con el perímetro, lo hacemos solos?
Un comentario en “Go con TDD: structs, methods y geometría”