DEV Community

Gino Luraschi
Gino Luraschi

Posted on

Escribir Golang como un Senior

#go

Introducción

Este post es una traduccion del post Write Go like a senior engineer, cuando lo leí me resultó interesante para compartirlo a partir del titulo, ya que toca varios puntos importantes cuando programamos en Go que son interesantes de profundizar. A esta lectura y comentarios como siempre, le agrego algunas cositas en base a mi experiencia.

Los parámetros en Go son pasados por valor

Por defecto, golang acepta pasajes por valor en sus funciones, lo que significa que lo que se recibe en la función es la copia del valor que pasamos. Esto impide que el valor de la estructura que estamos pasando mute dentro de la función a la que estamos llamando.
Veamos un ejemplo:

package main

import "fmt"

type Operation struct {
    Value float64
}

// Recibimos la operación y le "agregamos los impuestos"
func addTaxes(operation Operation) {
    operation.Value += operation.Value * 0.21
}

func main() {
    operation := Operation{Value: 100}
    // Llamamos a la función que agrega los impuestos
    addTaxes(operation)

    fmt.Printf("The total amount is: %f \n", operation.Value)
}
Enter fullscreen mode Exit fullscreen mode

Al terminar esta función, detectamos que el valor no muto, con esto queda demostrado que el pasaje es por valor. En cambio, si quisiéramos modificar el valor de la operación, deberíamos recibir el puntero (la referencia al struct operation) como lo vemos a continuación:

package main

import "fmt"

type Operation struct {
    Value float64
}
// Ahora la función recibe la referencia a una `Operation`
func addTaxes(operation *Operation) {
    operation.Value += operation.Value * 0.21
}

func main() {
    operation := Operation{Value: 100}
    // Ahora se envia la direccion de memoria de `operation`
    addTaxes(&operation)

    fmt.Printf("The total amount is: %f \n", operation.Value)
}
Enter fullscreen mode Exit fullscreen mode

En este caso si vemos que el valor muta ya que estamos cambiando el valor de la referencia, y no de una copia.

Usar punteros (pero no abusar)

Operador * y operador &

Como vimos en el ejemplo anterior, a la hora de manejarnos con punteros, podemos utilizar los símbolos * y &, pero que significa cada uno?

El operador * es la declaración de un puntero, por ejemplo podriamos tener la siguiente declaracion:

var operation *Operation
Enter fullscreen mode Exit fullscreen mode

Lo cual significa que la variable operation, es del tipo puntero de Operation.
En cambio, el símbolo & significa, pasar el valor del puntero de una variable, o la inicialización del puntero de una estructura:

// Aca creamos un puntero a una nueva estructura del tipo Operation
operation = &Operation{Value: 100}
// Aca pasamos la referencia, o locación de memoria de la variable operation
addTaxes(&operation)
Enter fullscreen mode Exit fullscreen mode

Aunque en el post original, diga

If you’re ever wondering “should I use a pointer here?” the answer is probably “Yes.” When in doubt, use a pointer.

Yo prefiero evitar punteros, esto lo voy a explicar en un post posterior porque hay tela que cortar.

Valor nil

El valor nil es el valor cero de los punteros en Go, una de las causa de los panic en go, debido a que a veces se trabaja con punteros nulos no chequeados. Esta es una de las causas por las cuales intento evitar punteros, porque se necesita ese check previo a la ejecución:

// `operation` es `nil`
var operation *Operation
// Al no estar chequeado, en el método nos arroja el panic
addTaxes(operation)
Enter fullscreen mode Exit fullscreen mode

Para evitar esto, tenemos que chequear y asegurarnos que el puntero no será nulo de la siguiente forma:

    // `operation` es `nil`
    var operation *Operation
    // Estamos chequeando para evitar `operation == nil`
    if operation != nil {
        addTaxes(operation)

        fmt.Printf("The total amount is: %f \n", operation.Value)
    }
Enter fullscreen mode Exit fullscreen mode

Declarar interfaces donde las vayas a usar

Las interfaces en Golang son explícitas, por lo que no es necesario declarar de donde se extienden, esto significa, que para poder extender de una interfaz, solo es necesario implementar sus métodos, veamos un ejemplo:

// UserRepository es el contrato que debemos cumplir
type UserRepository interface {
    FindByID(id string) User
    Save(user User)
}

// Esta será nuestra estructura que herede de la interfaz,
// notemos que no es necesario aclarar que extiende 
// de UserRepository
type SQLUserRepository struct {
}
// FindByID y Save son los métodos que implementamos para poder extender
func (S SQLUserRepository) FindByID(id string) User {
    panic("implement me")
}
func (S SQLUserRepository) Save(user User) {
    panic("implement me")
}
Enter fullscreen mode Exit fullscreen mode

Esa es la forma de crear interfaces, cómo podemos asegurarnos de que nuestra clase cumple el contrato, tenemos 2 opciones:

// La primera que suelo hacer, es crear un constructor
func NewSQL() UserRepository { // ← this is the line
    return SQLUserRepository{}
}
// La segunda, es una recomendación escrita en el post
var _ UserRepository = (*SQLUserRepository)(nil)
Enter fullscreen mode Exit fullscreen mode

Preferir tests en tablas, pero no hacer de más

En mi experiencia desarrollando Golang me he encontrado y he tenido que luchar con estos tests, no porque estuvieran mal hecho, sino que es muy difícil mantenerlos en el tiempo si la lógica de los métodos es compleja. Si, para probar entradas y salidas de tests funcionan perfectamente:

func Multiply(n1, n2 int) int {
    return n1 * n2
}
// Test de Multiply
func TestMultiply(t *testing.T) {
    cases := []struct {
        name           string
        n1             int
        n2             int
        expectedResult int
    }{
        {
            name:           "Multiply 2 numbers",
            n1:             2,
            n2:             4,
            expectedResult: 8,
        },
        {
            name:           "Multiply by 0",
            n1:             0,
            n2:             10,
            expectedResult: 0,
        },
    }

    for _, it := range cases {
        t.Run(it.name, func(t *testing.T) {
            result := Multiply(it.n1, it.n2)
            if result != it.expectedResult {
                t.Error("Result different, it'll fail")
                t.Fail()
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

En este caso, se puede ver un test simple, al cual a medida que agregamos casos, no se modificará tanto el código. Por lo cual para estos casos, creo que funcionan perfectos.

Cuándo evitar la tabla de tests?

Es muy fácil encontrar momentos en los que las tablas de tests se pueden evitar, mayormente cuando la complejidad del método es grande. En mi experiencia me he topado con tablas de tests en las cuales se empiezan a mockear elementos de los distintos casos de uso, y en donde cada caso apunta a algo diferente. Ya no alcanza con una entrada o salida, ahora tenemos que configurar algo más! Les propongo un ejemplo que me he encontrado:

func TestUserSaver_Execute(t *testing.T) {
    cases := []struct {
        name        string
        user        User
        saver       UserSaver
        expectedErr string
    }{
        //...
    }

    for _, it := range cases {
        t.Run(it.name, func(t *testing.T) {
            err := it.saver.Execute(it.user)
            if len(it.expectedErr) > 0 {
                if err != nil {
                    if err.Error() != it.expectedErr {
                        t.Errorf("We expected the error: %s", it.expectedErr)
                        t.Fail()
                    }
                } else {
                    t.Errorf("An error was expected %s", it.expectedErr)
                    t.Fail()
                }
            }
        })
    }
}
Enter fullscreen mode Exit fullscreen mode

En este caso la lógica es realmente compleja, para un método que realmente es simple. Por otro lado, si el caso de uso sigue incrementando su dependencia con otros componentes, los test se irán modificando. En un momento, si llegásemos a encontrar un bug, por experiencia propia, es muy complejo poder entender los tests y poder corregir ese problema.

Fuentes para seguir mejorando

Así como en el post original, quiero recomendarles una de las mejores guías para escribir go:

Conclusión

El objetivo de este post es aprender, y seguir aprendiendo, me parece una buena fuente para compartir con quien quiera seguir mejorando en su día a día en el lenguaje, toca puntos con los que me he cruzado y he luchado, por lo que dejó parte de mi experiencia por si a alguien le ayuda. Si conocen de otros puntos a sumar, bienvenidos sean, agradezco sus reacciones para poder motivarme a seguir compartiendo este conocimiento con todos los gophers de la comunidad hispana.

Top comments (0)