21
Drops #04: Desmistificando ponteiros no Golang!
E ae dev, tudo bem com você?
Agora que FINALMENTE finalizei o MBA, aproveitei o tempo livre para dedicá-lo aos estudos do Golang.
Confesso que eu estou apaixonado pela linguagem - ainda dando os primeiros passos claro - mas creio que já dá pra compartilhar uma parada muito massa da linguagem (e como ela implementa) que são os ponteiros!
Bora pro post?
Ah! mas antes disso... Esse post faz parte de uma série de artigos "drops" que tenho aqui! Veja a lista:
Um ponteiro (ou apontador) nada mais é do que uma variável que, ao invés de armazenar um valor (true, "hello world"), ela armazena um endereço que está alocado na memória.
Vamos entender a imagem a seguir:
Basicamente, bem a grosso modo, a memória é constituída por elementos que armazenam informações.
O endereço é uma posição onde os dados serão colocados (geralmente expressos em números hexadecimais). Eles podem conter apenas uma única informação.
O dado, por sua vez, é a informação presente em cada posição na memória.
Quando criamos uma variável, ela recebe um endereçamento na memória e através dessa variável podemos armazenar valores que serão alocados nesse endereço:
Aqui podemos ler o seguinte:
A variável a
que possui o endereço de memória 0xc000192020
está armazenando o dado 10
na posição deste endereço.
É muito simples definir um ponteiro, basta adicionar o *
junto ao tipo do ponteiro que estamos criando:
package main
import "fmt"
func main() {
var p *int
fmt.Println(p) // output: <nil>
}
💡 O tipo do ponteiro indica qual é o tipo de dado que esse ponteiro irá manipular. No caso acima, criamos um ponteiro que pode "apontar" para endereços na memória de variáveis que armazenam dados do tipo inteiro.
No Go temos o conceito de zero value que, ao definir uma variável sem atribuir um valor para ela, o Go irá atribuir um valor padrão para essa variável. Cada tipo possui o seu valor padrão (0
para int
, false
para bool
...), no caso do nosso ponteiro, o zero value será nil
dado que não atribuímos nenhum valor para ele. Ou melhor dizendo: nenhum endereço!
E como podemos definir um endereço para o ponteiro? 🤔
Primeiro, criaremos outra variável no nosso código, e então, vamos atribuir o endereço dela para o ponteiro dessa forma:
package main
import "fmt"
func main() {
var p *int
i := 10
p = &i // atribuindo o endereço de i para o ponteiro
fmt.Println(p) // output: algo como 0xc000192020
}
💡 O
&
usado antes da variável, indica que queremos obter o endereço na memória daquela variável.
A representação gráfica do que fizemos aqui está na imagem a seguir:
💡 Note que o ponteiro que criamos, também possui o seu próprio endereço na memória!
Beleza, entendi! Até aqui o ponteiro já tá "apontando" pra um endereço... mas como eu faço pra saber o que esse endereço aí tá armazenando? 🤔
Basta utilizar o operador *
junto ao ponteiro (*p
). Esse é o processo de desreferenciar o ponteiro para que, ao invés dele retornar o endereço armazenado, ele ir lá naquele endereço e a partir de lá retornar o valor:
package main
import "fmt"
func main() {
var p *int
i := 10
p = &i
fmt.Println(p) // output: 0xc000192020
fmt.Println(*p) // output: 10
}
Uma vez que desreferenciamos o ponteiro, podemos manipular o dado que está armazenado naquele endereço:
package main
import "fmt"
func main() {
var p *int
i := 10
p = &i
fmt.Println(p) // output: 0xc000192020
fmt.Println(*p) // output: 10
*p = 20
fmt.Println(i) // output: 20
fmt.Println(*p) // output: 20
}
A representação gráfica do que fizemos aqui está na imagem a seguir:
No Go, por padrão, tudo é Pass By Value. Isso significa que, quando passamos uma variável como parâmetro de uma função, essa variável é "duplicada" na memória e o que fazemos dentro do escopo da função acontece apenas no escopo da função:
package main
import "fmt"
func increment(a int) int {
a++
fmt.Println(a) // output: 11
return a
}
func main() {
x := 10
increment(x)
fmt.Println(x) // output: 10
}
Como é realizada uma "cópia" da variável na memória, isso tem um custo, que pode se tornar problemático quando lidamos com um grande volume de dados nessa variável. Para economizarmos essa operação, é possível trabalhar com Pass By Reference com a ajuda de ponteiros:
package main
import "fmt"
func increment(a *int) {
*a++
fmt.Println(*a) // output: 11
fmt.Println(a) // output: 0xc0000180b0
}
func main() {
x := 10
increment(&x)
fmt.Println(x) // output: 11
fmt.Println(&x) // output: 0xc0000180b0
}
Perceba que a função increment
não retorna mais o valor manipulado e que além disso, passou a exigir um ponteiro/endereço de memória como parâmetro (a *int
).
Dessa maneira, o parâmetro da função increment
não foi duplicado na memória e passamos a trabalhar com a referência da variável x
(que está fora do escopo da função increment
).
A grosso modo, podemos trabalhar com ponteiros uma vez que lidamos com um volume grande de dados ou simplesmente querer trabalhar com Pass By Reference ao invés de Pass By Value.
É importante entender que existem pontos positivos e negativos ao trabalhar com ponteiros. Nós podemos e devemos usá-los mas precisamos ter certeza do que estamos fazendo.
Ou seja: depende! HAHA Cabe a você avaliar os melhores cenários.
Bem, é isso, por hoje, é só!
Quero te agradecer por chegar até aqui, e queria lhe pedir também para me encaminhar as suas dúvidas, comentários, críticas, correções ou sugestões sobre a publicação.
Deixe seu ❤️ se gostou ou um 🦄 se esse post te ajudou de alguma maneira! Não se esqueça de ver os posts anteriores e me siga para maaaais conteúdos.
Até!
21