DEV Community

Cover image for Não use funções puras com Go
Mateus Vinícius
Mateus Vinícius

Posted on

Não use funções puras com Go

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, lib de ORM muito popular.

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)
}
Enter fullscreen mode Exit fullscreen mode

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.

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
}
Enter fullscreen mode Exit fullscreen mode

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.

Top comments (0)