Continuando a nossa série de artigos sobre a linguagem de programação Go, nós vamos falar sobre ponteiros na Golang. Neste artigo vamos ver o que são ponteiros, quais as vantagens do uso de ponteiros em nossos programas e como podemos utilizá-los na Golang.
O que são ponteiros?
Ponteiros na Golang e também em outras linguagens de programação que disponham desse recurso, são variáveis que armazenam endereços de memória. Cada variável em um programa é armazenada em um endereço de memória específico, portanto, pode ser acessada diretamente pelo seu endereço de memória. Um ponteiro armazena o endereço de memória de outra variável, permitindo que o programa acesse ou modifique o conteúdo dessa variável indiretamente, ou seja, sem usar o nome da variável diretamente.
Os ponteiros são usados para alocar memória dinamicamente, passar argumentos para funções por referência, criar estruturas de dados complexas, manipular cadeias de caracteres e para acessar recursos de hardware diretamente. A capacidade de usar ponteiros é uma das principais características de linguagens de programação de baixo nível, como C e C++, mas também é suportada em muitas outras linguagens de programação modernas.
Curso Go Básico
Conhecer o cursoQuais as vantagens e desvantagens dos ponteiros?
O uso de ponteiros na Golang oferece várias vantagens:
- Eficiência de memória: Em algumas situações, como na alocação dinâmica de memória, o uso de ponteiros pode ser mais eficiente do que a alocação estática de memória, permitindo que o programa acesse apenas a quantidade necessária de memória.
- Flexibilidade: O uso de ponteiros permite que os programas criem estruturas de dados complexas e dinâmicas, permitindo uma maior flexibilidade na programação.
- Passagem por referência: Os ponteiros permitem que as funções recebam argumentos por referência, o que significa que a função pode modificar o valor do argumento original. Isso é particularmente útil quando se trabalha com abundância de dados, pois evita a necessidade de copiar grandes blocos de dados entre as funções.
- Acesso a recursos de hardware: O uso de ponteiros permite que os programas acessem diretamente os recursos de hardware, como a memória ou os registradores do processador, permitindo uma maior otimização de código e desempenho.
Porém, nem tudo são flores, embora os ponteiros na Golang sejam uma ferramenta poderosa, seu uso incorreto pode levar a vários problemas, como, por exemplo:
- Segurança: Os ponteiros podem ser usados para acessar diretamente a memória do sistema, o que pode ser um risco de segurança. Se um programa permitir que um ponteiro acesse áreas de memória que não são esperadas, pode ocorrer corrupção de memória ou violação de segurança.
- Acesso não autorizado: O uso incorreto de ponteiros pode levar a tentativas de acesso não autorizado aos recursos de hardware, incluindo a memória do sistema, o que pode causar problemas de segurança ou falhas do sistema.
- Gerenciamento de memória: O uso de ponteiros exige que o programador gerencie a alocação e desalocação de memória manualmente, o que pode ser propenso a erros. Se um programa não desalocar a memória corretamente, pode levar a vazamentos de memória, resultando em consumo excessivo de recursos do sistema.
- Complexidade: O uso de ponteiros pode tornar o código mais complexo e difícil de entender, especialmente para programadores menos experientes. Isso pode levar a erros de programação e dificuldades para manter e atualizar o código.
Em geral, é importante usar ponteiros com cuidado e entender os riscos e benefícios associados ao seu uso. É necessário ter um conhecimento sólido de gerenciamento de memória e de programação em geral para evitar problemas de segurança e garantir que o código seja eficiente e seguro.
Trabalhando com ponteiros na Golang
Agora que já vimos o que são e quais as vantagens e desvantagens de trabalhar com os ponteiros, vamos colocar a mão no código e entender como utilizar esse recurso na linguagem de programação Go.
Como declarar um ponteiro?
Para declarar ponteiros na Golang, usamos a seguinte sintaxe var nomePonteiro *tipo
. Então, caso queiramos declarar um ponteiro que aponta para valores do tipo inteiro faremos da seguinte maneira var ponteiroInteiro *int
.
Vejamos o exemplo em um código completo:
package main
import (
"fmt"
)
func main() {
var ptr *int
fmt.Println(ptr)
fmt.Printf("%T\n", ptr)
}
O código acima ao ser executado irá exibir o seguinte resultado no terminal:
<nil>
*int
Vamos entender o que está acontecendo nesse código, temos inicialmente a declaração do pacote e a importação do módulo fmt
, dentro da nossa função main
, temos a declaração de um ponteiro chamado ptr
que aponta para valores do tipo inteiro com a instrução var ptr *int
e logo em seguida realiza a exibição do valor e o tipo da variável ptr
.
Veja que a exibição do valor de ptr
resultou na saída ``, esse nil
significa que o nosso ponteiro não aponta para lugar nenhuma, ou seja, nós temos o ponteiro declarado, mas que não aponta para nenhuma posição de memória, esse é o zero value de um ponteiro. Na segunda impressão temos o resultado *int
, que demonstra que a nossa variável ptr
de fato é um ponteiro de inteiros.
Como apontar para uma posição de memória?
Na golang, para podermos saber a posição de memória de uma variável utilizamos o operador &
(E comercial). Então, vamos criar duas variáveis, uma que possui um valor e a outra será um ponteiro que aponta para a posição de memória da primeira variável.
package main
import (
"fmt"
)
func main() {
var a int = 10
var ptr *int = &a
fmt.Printf("Endereço de a: %p\n", &a)
fmt.Printf("Endereço de ptr: %p\n", ptr)
}
No código acima declaramos uma variável a
do tipo int
sido inicializada com o valor 10
, logo em seguida declaramos a variável ptr
que é um ponteiro de inteiro e que aponta para a posição de memória da variável a
e por fim exibimos no terminal a posição de memória de ambas as variáveis. Ao executar esse código teremos o seguinte resultado:
Endereço de a: 0xc00001c1e0
Endereço de ptr: 0xc00001c1e0
Veja que ambas as variáveis apontam para a mesma posição de memória.
Acessando o valor apontado por ponteiros na Golang
Também é possível acessarmos o valor apontado por um ponteiro utilizando o operador *
antes do nome do ponteiro. Por exemplo:
package main
import (
"fmt"
)
func main() {
var a int = 10
var ptr *int = &a
fmt.Printf("Endereço de a: %p\n", &a)
fmt.Printf("Valor de a: %d\n", a)
fmt.Printf("Endereço de ptr: %p\n", ptr)
fmt.Printf("Valor de ptr: %d\n", *ptr)
}
Ao executar o código acima teremos as seguinte saída no terminal:
Endereço de a: 0xc000122000
Valor de a: 10
Endereço de ptr: 0xc000122000
Valor de ptr: 10
Veja que ao utilizarmos a sintaxe *ptr
conseguimos obter o valor 10
que é o valor armazenado na posição de memória apontada pelo ponteiro ptr
. Essa operação de colocar o operador *
antes do nome do ponteiro é chamada de “desreferência de ponteiro”, ou seja, quando fizemos *ptr
nós desrefrerênciamos o ponteiro ptr
para acessar o valor que estava armazenado na posição de memória apontada pelo ponteiro ptr
.
Alterando o valor de uma posição de memória
Agora que sabemos que podemos acessar o valor de uma posição de memória através da operação de desreferência, nós podemos alterar o valor armazenado nessa posição de memória, veja o exemplo:
package main
import (
"fmt"
)
func main() {
var a int = 10
var ptr *int = &a
fmt.Printf("Endereço de a: %p\n", &a)
fmt.Printf("Valor de a: %d\n", a)
fmt.Printf("Endereço de ptr: %p\n", ptr)
fmt.Printf("Valor de ptr: %d\n", *ptr)
*ptr = 20
fmt.Printf("Novo valor de a: %d\n", a)
fmt.Printf("Novo valor de ptr: %d\n", *ptr)
}
Ao executar o código acima, teremos a seguinte saída no terminal:
Endereço de a: 0xc00001c1e0
Valor de a: 10
Endereço de ptr: 0xc00001c1e0
Valor de ptr: 10
Novo valor de a: 20
Novo valor de ptr: 20
Veja que ao utilizar a sintaxe *ptr = 20
, nós alteramos o valor armazenado na posição de memória apontada pelo ponteiro ptr
para o valor 20
. Com isso, a variável a
também foi atualizada para o valor 20
. É importante lembrar que ao utilizar ponteiros para alterar valores de variáveis, devemos ter cuidado para não acessar áreas de memória não autorizadas e causar problemas de segurança ou falhas do sistema.
Passagem por valor e por referência
A passagem por valor e por referência é uma questão importante quando se trabalha com ponteiros na Golang. Na linguagem Go, por padrão, todas as variáveis são passadas por valor para as funções, ou seja, a função recebe uma cópia do valor da variável. Porém, é possível passar um ponteiro para a função, permitindo que a função acesse e modifique o valor original da variável. Isso pode ser útil quando se trabalha com abundância de dados e se deseja evitar a cópia desnecessária de dados entre as funções. É importante lembrar que o uso de ponteiros para passagem de argumentos pode tornar o código mais complexo e difícil de entender, especialmente para programadores menos experientes.
Para ficar mais claro vejamos o seguinte exemplo:
package main
import (
"fmt"
)
func mudaValor(a int) {
a = 20
}
func main() {
var a int = 10
fmt.Printf("Valor de a: %d\n", a)
mudaValor(a)
fmt.Printf("Valor de a: %d\n", a)
}
Ao executarmos esse código teremos o seguinte resultado:
Valor de a: 10
Valor de a: 10
Veja que mesmo após a chamada da função mudaValor
, o valor da variável a
continuou o mesmo, isso acontece, pois a função mudaValor
recebe uma cópia do valor contido na variável a
e não a posição de memória da variável a
. Por isso ao realizarmos a = 20
o valor da variável a
continuou o mesmo. Para conserguimos de fato alterar o valor da variável teremos que mudar o nosso código para que a função mudaValor
receba um ponteiro como parâmetro, dessa forma faremos com que nossa função trabalhe com passagem por referência e não com passagem por valor.
package main
import (
"fmt"
)
func mudaValor(a *int) {
*a = 20
}
func main() {
var a int = 10
fmt.Printf("Valor de a: %d\n", a)
mudaValor(&a)
fmt.Printf("Valor de a: %d\n", a)
}
Ao executarmos o código acima teremos o seguinte resultado:
Valor de a: 10
Valor de a: 20
Veja que agora ao chamarmos a função mudaValor
, o valor contido na variável a
de fato foi alterado de 10
para 20
.
Conclusão
Neste artigo, vimos o que são ponteiros em Golang, suas vantagens e desvantagens, e como trabalhar com eles na linguagem. Embora os ponteiros possam ser uma ferramenta poderosa, seu uso incorreto pode levar a problemas de segurança e desempenho. É importante entender bem o gerenciamento de memória e de programação em geral para evitar esses problemas e garantir a eficiência e segurança do código.
Por fim, caso queira aprender mais sobre a Go e sua infinidade de recursos saiba que aqui na TreinaWeb temos o curso Go Básico que possui 05h30 de vídeo e um total de 37 exercícios.
Veja quais são os tópicos abordados durante o curso Go Básico:
- Compreender a sintaxe básica da Golang;
- Compreender conceitos básicos envolvidos na Go, como ponteiros de memória;
- Utilizar as estruturas básicas da linguagem, como declaração de variáveis;
- Utilizar as principais estruturas de conjuntos da Go, como arrays, slices e maps;
- Entender as principais funções na Golang que são built-in, como make(), new(), panic(), recover() e defer;
- Organizar o código em pacotes e utilizar os principais pacotes disponibilizados pela linguagem;
- Entender como podemos utilizar concorrência com a Golang, inclusive com técnicas como os channels;
- Entender o que são as structs na Go e como podemos utilizar um pouco de orientação a objetos com a linguagem;
- Realizar operações de I/O no sistema operacional, como a criação e escrita de arquivos.