Ponteiros, stack e heap em Go
Tags: computerscience, memoryallocation, go
Published: 2024-03-03 21:20:56

## 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:
```golang
// 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) 
```

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:
```golang
var string name = "Matt"
var baz *string = &name
```

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_.
```golang
var name string = "Matt"
var baz *string = &name
fmt.Println(*baz) // Resultado: "Matt"
```

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_:
```golang
package main

import "fmt"

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

func greetings(name string) {
	fmt.Println(name)
}
```

Executando o código, esse seria o comportamento da stack:
[![Stack](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zchfpnp3v09fhciysxhg.png)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/zchfpnp3v09fhciysxhg.png)

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rantyl6y8rn9m960sm4x.png)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rantyl6y8rn9m960sm4x.png)

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:
```golang
package main

import "fmt"

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

func greetings(name *string) {
	fmt.Println(*name)
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/09czskrt7dmai5918mev.png)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/09czskrt7dmai5918mev.png)

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:
```golang
package main

import "fmt"

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

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

func greetings(name *string) {
	fmt.Println(*name)
}
```

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kddxt8as3ns5o8i18ylx.png)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/kddxt8as3ns5o8i18ylx.png)

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.