Ponteiros, stack e heap em Go

antiduhring

Mateus Vinícius

Posted on March 3, 2024

Ponteiros, stack e heap em Go

Pointers

Ponteiros são variáveis cujo valor é um endereço na memória, e esse endereço pode conter qualquer tipo de valor - uma string, um int, uma struct e etc.

A sintaxe básica de ponteiros em Go é relativamente simples, mas iremos entrar nos detalhes ao decorrer do texto:

// declarando uma variável do tipo int
var foo int = 3 

// exibindo na tela o endereço na memória da variável "foo"
// O resultado será algo como: 0xc000110010
fmt.Println(&foo) 

// declarando uma variável do tipo ponteiro
// cujo endereço apontado é um variável do tipo int.
// estamos apontando para o endereço da variável foo
var bar *int = &foo

// exibindo na tela o valor da variável "bar", que é um endereço na memória. 
// o resultado será o mesmo do Println anterior
// já que ambos apontam para o mesmo endereço na memória
fmt.Println(bar) 

// exibindo na tela o valor original contido no endereço
// Resultado: 3
fmt.Println(*bar) 
Enter fullscreen mode Exit fullscreen mode

Primeiro é importante diferenciar os diferentes usos do operador *, também conhecido como star operator.

Star operator num tipo é chamado de pointer type, e indica que aquela variável é o ponteiro para algo - como int, string etc -, esse algo é chamado de base. Por exemplo, o código abaixo indica que a variável baz é um pointer type de base string:

var string name = "Matt"
var baz *string = &name
Enter fullscreen mode Exit fullscreen mode

Já se o uso do star operator seguido de uma variável é uma forma de obter o valor original do endereço na memória que o ponteiro está salvando. Chamamos isso de dereferencing.

var name string = "Matt"
var baz *string = &name
fmt.Println(*baz) // Resultado: "Matt"
Enter fullscreen mode Exit fullscreen mode

Apesar da sintaxe simples, o uso apropriado de ponteiros no Go pode se tornar complexo devido ao fato de não ser sempre claro qual será o comportamento do compilador na alocação de memória. Basicamente um ponteiro pode ser salvo na stack ou no heap, e a escolha de um ou outro é muitas vezes implícita.

Stack

Stack - ou pilha - é uma estrutura de dado que salva cada bloco de código que é executado numa goroutine, de forma isolada e ordenada, e cada bloco é chamado de frame. Exemplo, no código abaixo temos uma função main, que chama uma função greetings, que chama a função fmt.Println:

package main

import "fmt"

func main() {
    name := "Matt"
    greetings(name)
}

func greetings(name string) {
    fmt.Println(name)
}
Enter fullscreen mode Exit fullscreen mode

Executando o código, esse seria o comportamento da stack:
Stack

Após a execução do frame terminar, a stack automaticamente se limpa sozinha, jogando fora toda a memória alocada em cada frame e finalizando nosso programa.
Stack

Normalmente frames não podem acessar endereços na memória de fora dele, então se um ponteiro for criado num frame e apenas acessado nesse frame, aquela memória normalmente será alocada dentro da stack, no próprio frame, e será zerada no momento em que esse frame terminar a sua execução e for removido da stack. O mesmo vale caso o ponteiro seja criado num frame e passado pra baixo na execução dos frames filhos, por exemplo no código abaixo:

package main

import "fmt"

func main() {
    name := "Matt"
    greetings(&name)
}

func greetings(name *string) {
    fmt.Println(*name)
}
Enter fullscreen mode Exit fullscreen mode

A função greetings recebe um ponteiro que aponta para o endereço na memória da variável name, e essa variável está salva dentro do frame main, que não será removido da stack até a função greetings e fmt.Println terminarem sua execução, então é seguro manter a variável salva lá.
Stack

Importante mencionar que, caso a variável passada não seja um ponteiro e sim uma string, por exemplo, o valor é copiado ao passar de um frame para o outro, pois, como dito anteriormente, frames normalmente não podem acessar memória de fora do seu próprio bloco de execução.

Dado o comportamento de self cleaning, ou seja, de remover da stack o frame assim que ele finaliza sua execução, a stack não precisa de ajuda do garbage collector, o que melhora a performance do código.

Heap

Mas existem situações em que não seria seguro manter a memória salva no próprio frame, pois ele pode ser acessado de fora mesmo após o frame ser removido da stack, o que causaria a perda do seu valor, retornando apenas nil. Por exemplo, no trecho abaixo:

package main

import "fmt"

func main() {
    name := createName()
    greetings(name)
}

func createName() *string {
    name := "Matt"
    return &name
}

func greetings(name *string) {
    fmt.Println(*name)
}
Enter fullscreen mode Exit fullscreen mode

A função createName cria o ponteiro que aponta para a variável name, mas ao finalizar sua execução a função greetings ainda precisa acessar esse valor. O comportamento da stack, nesse caso, seria esse:
Stack

Mas se o frame da função createName foi removido da stack após sua execução, o que significa que toda a memória foi zerada, como a função greetings ainda assim conseguiu acessá-lo? A resposta é que, nesse caso, a memória não foi alocada dentro do frame na stack, mas sim num lugar de fora, chamado heap.

O heap é o local que guarda essas memórias alocadas que precisam ser compartilhadas entre frames e até entre stacks, sem que sejam apagadas após a execução do frame que as criou.

Dado esse comportamento pode parecer muito tentador usar apenas memória alocada no heap, mas, como não há o comportamento de self cleaning, o heap precisa de ajuda do garbage collector do Go, e isso têm um custo computacional, criando cada vez mais latência no programa.

Mas nem sempre a alocação em memória do Go é tão previsível assim, por isso destaquei o "normalmente" ao descrever os comportamentos de alocação na stack e no heap, mas isso será melhor explorado num momento futuro. Por ora basta entender que cada tipo de alocação tem suas vantagens e desvantagens, e que usar ponteiros, apesar de parecer simples, pode trazer certa complexidade e custos por trás.

💖 💪 🙅 🚩
antiduhring
Mateus Vinícius

Posted on March 3, 2024

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related

Ponteiros, stack e heap em Go
computerscience Ponteiros, stack e heap em Go

March 3, 2024