Não use funções puras com Go
Tags: webdev, go, functional
Published: 2023-10-22 15:44:03

Um mantra muito comum no mundo do desenvolvimento é que funções puras são sempre melhores, mas isso nem sempre é verdade, pelo menos não no ecossistema do Go, e esse é um padrão amplamente adotado pela comunidade.

## O que é uma função pura?
Uma função pura é aquela que segue princípios de determinismo e imutabilidade. Ela retorna o mesmo resultado para o mesmo conjunto de argumentos de entrada, sem depender de variáveis externas, tornando o comportamento previsível e fácil de entender. Além disso, não modifica o estado de variáveis ou objetos fora de seu escopo, que é o chamado _side effect_, mas sim cria e retorna um novo valor com base em seus argumentos, evitando efeitos colaterais indesejados. 

## Qual o padrão usado no lugar das funções puras?
Go te permite acessar ponteiros diretamente, assim como linguagens _low-level_ como C e C++, e se tornou um padrão da comunidade dar preferência a funções que recebem como argumento um _ponteiro_ (isto é, o endereço na memória) para algum valor, como uma _struct_, e manipular esse valor diretamente dentro da função.

Um exemplo desse padrão pode ser visto no [GORM](https://github.com/go-gorm/gorm), lib de ORM muito popular. 

```golang
package main

import (
  "gorm.io/gorm"
  "gorm.io/driver/sqlite"
)

type Product struct {
  gorm.Model
  Code  string
  Price uint
}

func main() {
  db, err := gorm.Open(sqlite.Open("test.db"), &gorm.Config{})
  if err != nil {
    panic("failed to connect database")
  }

  // Migrate the schema
  db.AutoMigrate(&Product{})

  product := Product{
    Code: "D42", 
    Price: 100
  }
  // Create
  db.Create(&product)

  // Read
  db.First(&product, 1) // find product with integer primary key
  db.First(&product, "code = ?", "D42") // find product with code D42

  // Update - update product's price to 200
  db.Model(&product).Update("Price", 200)
  // Update - update multiple fields
  db.Model(&product).Updates(Product{Price: 200, Code: "F42"}) // non-zero fields
  db.Model(&product).Updates(map[string]interface{}{"Price": 200, "Code": "F42"})

  // Delete - delete product
  db.Delete(&product, 1)
}
```

A variável _product_ é uma struct cujo ponteiro é passado como argumento nas funções de update, create e etc, dessa forma, o retorno da operação já é preenchido dentro desse variável. No caso do create, por exemplo, a variável _product_ passaria a ter todas as propriedades do registro que foi criado (ex: um uuid gerado automaticamente pelo banco de dados). No caso do update, o valor da struct é modificado para corresponde aos novos valores da operação de atualização.

O padrão do ecossistema do Go é reaproveitar variáveis declaradas no mesmo escopo, por isso a preferência no uso de ponteiros.

## Qual a vantagem de usar funções impuras em Go?
Em suma, performance. Uma função pura retorna uma nova struct contendo as propriedades do registro de cada uma das operações, então o create criaria uma nova struct contendo os dados criados, o update criaria uma nova struct contendo os dados modificados e assim sucessivamente. 

```golang
func GetUserBalance(c *gin.Context) {
	id := c.Param("id")

	if id == "" {
		sendError(c, http.StatusBadRequest, "missing id parameter")
		return
	}

	user, err := getUser(id, db.Client)

	if err != nil {
		sendError(c, http.StatusInternalServerError, err.Error())
		return
	}

	sendSuccess(c, http.StatusOK, gin.H{"balance": user.Wallet.Balance})
}

func getUser(id string, client *gorm.DB) (schema.User, error) {
	user := schema.User{}

	if err := client.Preload("Wallet").Where("id = ?", id).First(&user).Error; err != nil {
		return user, err
	}

	return user, nil
}
```

No exemplo acima getUser é uma função pura que, dado um id e um client do banco, retorna sempre uma nova cópia da struct `schema.User{}`.

Se cada operações gera uma cópia de uma struct então nos encontramos em um cenário onde, durante um dado processamento, vão haver diversas cópias da mesma struct salvas na memória, mesmo que só precisemos de uma delas ao final da operação. Isso é um desperdício de memória e vai diretamente contra os objetivos do Go em ser uma linguagem performática, otimizada e com baixo consumo de RAM.

## Conclusão
Funções puras são extremamente úteis num cenário de paradigma funcional, elas são fáceis de testar e seu comportamento é previsível, mas toda solução possui _tradeoffs_ e, nesse caso, é sacrificado o consumo de memória. No ecossistema do Go, onde o baixo consumo de RAM é padrão, funções puras fazem pouco sentido e desperdiçam o potencial da linguagem que nos fornece tantas ferramentas para lidar com ponteiros.