En el mundo de la programación, un puntero es una variable que almacena la dirección de memoria de otra variable. Mientras que las variables regulares son marcadores convenientes para la memoria, un puntero te permite almacenar la ubicación de estos marcadores. Piensa en un puntero como una entrada en un índice de libro: no solo ocupa su propio espacio en la página, sino que también te guía a la sección específica (la dirección de memoria) que contiene la información.
Declaración de Punteros
Para declarar un puntero, usamos la sintaxis var nombre_variable *tipo, donde *tipo denota que la variable es de tipo puntero y apunta a un valor de tipo tipo.
var puntero_entero *int // Puntero a un entero
var puntero_flotante *float32 // Puntero a un flotante
Recordemos que el operador & se utiliza para obtener la dirección de memoria de una variable. Esta dirección es, en esencia, el valor que almacena un puntero.
package main
import "fmt"
func main() {
var numero int = 20 // Variable original
var puntero_numero *int // Declaración del puntero
puntero_numero = &numero // Asignamos la dirección de 'numero' al puntero
fmt.Printf("La dirección de memoria de 'numero' es: %x\n", &numero)
fmt.Printf("La dirección de memoria almacenada en 'puntero_numero' es: %x\n", puntero_numero)
fmt.Printf("El valor al que apunta 'puntero_numero' es: %d\n", *puntero_numero) // Dereferenciación para obtener el valor
}
El tipo de una variable puntero es *Tipo, y este puntero contiene la dirección de memoria de una variable de tipo Tipo. La característica principal de un puntero es que almacena la dirección de otra variable, permitiendo la manipulación indirecta de esa variable.
- El operador
&, cuando se aplica a una variable, devuelve su dirección de memoria. - El operador
*, utilizado a la izquierda de una asignación, se refiere a la variable a la que apunta el puntero. Cuando se usa a la derecha de una asignación o en una expresión, se utiliza para obtener el valor almacenado en la dirección de memoria apuntada por el puntero (esto se conoce como desreferenciación).
Tipos de Punteros
Cada tipo de dato tiene su correspondiente tipo de puntero:
package main
import "fmt"
func main() {
cadena := "Ejemplo"
entero := 1
booleano := false
flotante := 3.14
fmt.Printf("Tipo de &cadena: %T\n", &cadena)
fmt.Printf("Tipo de &entero: %T\n", &entero)
fmt.Printf("Tipo de &booleano: %T\n", &booleano)
fmt.Printf("Tipo de &flotante: %T\n", &flotante)
}
La salida mostrará:
Tipo de &cadena: *string
Tipo de &entero: *int
Tipo de &booleano: *bool
Tipo de &flotante: *float64
Fundamentalmente, el tipo de un puntero es el tipo del valor al que apunta.
Punteros Nulos
Un puntero que no ha sido asignado a la dirección de ninguna variable se considera un punterro nulo. En Go, este valor nulo se representa con la constante nil. Es conceptualmente similar a null, None o NULL en otros lenguajes, indicando la ausencia de un valor o dirección válida.
Se puede verificar si un puntero es nulo de la siguiente manera:
if puntero != nil { // El puntero no es nulo
// ...
}
if puntero == nil { // El puntero es nulo
// ...
}
Ejemplo práctico:
package main
import "fmt"
func main() {
texto := "Un texto"
var puntero_texto *string
fmt.Println("Valor inicial de puntero_texto:", puntero_texto) // Imprime <nil>
puntero_texto = &texto
fmt.Println("Valor de puntero_texto después de la asignación:", puntero_texto) // Imprime la dirección de memoria
}
</nil>
Salida:
Valor inicial de puntero_texto: <nil>
Valor de puntero_texto después de la asignación: 0xc00003c250 // La dirección variará
Es importante notar que la comparación con nil se realiza usando los operadores de igualdad (==) o desigualdad (!=), no con palabras clave como is en Python.
Operaciones con Punteros
Acceder al valor al que apunta un puntero se realiza mediante el operador de desreferenciación (*).
package main
import (
"fmt"
)
func main() {
valor_original := 255
puntero_valor := &valor_original
fmt.Println("Dirección de valor_original:", puntero_valor)
fmt.Println("Valor al que apunta puntero_valor:", *puntero_valor)
}
Salida:
Dirección de valor_original: 0xc000014088 // La dirección variará
Valor al que apunta puntero_valor: 255
Podemos modificar el valor de la variable original a través de su puntero:
package main
import (
"fmt"
)
func main() {
valor_original := 255
puntero_valor := &valor_original
fmt.Println("Dirección de valor_original:", puntero_valor)
fmt.Println("Valor al que apunta puntero_valor (antes):", *puntero_valor)
*puntero_valor++ // Incrementamos el valor a través del puntero
fmt.Println("Nuevo valor de valor_original:", valor_original)
}
Salida:
Dirección de valor_original: 0xc0000aa058 // La dirección variará
Valor al que apunta puntero_valor (antes): 255
Nuevo valor de valor_original: 256
Al incrementar el valor referenciado por el puntero, modificamos directamente la variable original.
Punteros como Parámetros de Función
Los punteros son útiles para modificar variables fuera del alcance de una función. Como Go pasa los argumentos por valor por defecto, pasar un puntero permite a la función operar sobre la dirección de memoria original.
package main
import (
"fmt"
)
func modificarValor(val *int) {
*val = 55 // Modifica el valor en la dirección de memoria proporcionada
}
func main() {
a := 58
fmt.Println("Valor de 'a' antes de llamar a la función:", a)
puntero_a := &a
modificarValor(puntero_a)
fmt.Println("Valor de 'a' después de llamar a la función:", a)
}
Salida:
Valor de 'a' antes de llamar a la función: 58
Valor de 'a' después de llamar a la función: 55
Si la función modificarValor recibiera val int en lugar de val *int, operaría sobre una copia del valor, y la variable original a no se vería afectada.
package main
import (
"fmt"
)
func modificarValorPorCopia(val int) {
val = 55 // Modifica una copia local
}
func main() {
b := 58
modificarValorPorCopia(b)
fmt.Println("Valor de 'b' después de llamar a la función por copia:", b)
}
Salida:
Valor de 'b' después de llamar a la función por copia: 58
Usar punteros como argumentos de función permite modificar eficientemente tipos de datos de valor, evitando la asignación de memoria adicional para copias.
Modificando Arreglos con Punteros
Para modificar los elementos de un arreglo dentro de una función y que los cambios sean visibles externamente, podemos pasar un puntero al arreglo.
package main
import (
"fmt"
)
func modificarArreglo(arr *[3]int) {
(*arr)[0] = 90 // Modifica el primer elemento del arreglo apuntado
}
func main() {
mi_arreglo := [3]int{89, 90, 91}
modificarArreglo(&mi_arreglo)
fmt.Println(mi_arreglo)
}
Salida:
[90 90 91]
Alternativamente, y de manera más idiomática en Go, se pueden utilizar slices (rebanadas), que son tipos de referencia y permiten la modificación directa:
package main
import (
"fmt"
)
func modificarSlice(sls []int) {
sls[0] = 90 // Modifica el primer elemento del slice
}
func main() {
mi_arreglo := [3]int{89, 90, 91}
modificarSlice(mi_arreglo[:]) // Pasamos un slice que referencia al arreglo
fmt.Println(mi_arreglo)
}
Salida:
[90 90 91]
Mientras que pasar un puntero a un arreglo funciona, usar slices es generalmente más flexible y común para este propósito.
Punteros a Punteros
Un puntero puede apuntar a la dirección de memoria de otro puntero. Esto se conoce como un puntero a puntero.
package main
import "fmt"
func main() {
var a int
var ptr *int // Puntero a int
var pptr **int // Puntero a puntero a int
a = 3000
ptr = &a // ptr apunta a la dirección de a
pptr = &ptr // pptr apunta a la dirección de ptr
fmt.Printf("Valor de a: %d\n", a)
fmt.Printf("Valor al que apunta ptr (*ptr): %d\n", *ptr)
fmt.Printf("Valor al que apunta pptr (**pptr): %d\n", **pptr)
}
Salida:
Valor de a: 3000
Valor al que apunta ptr (*ptr): 3000
Valor al que apunta pptr (**pptr): 3000
Al modificar el valor a través del puntero a puntero, los cambios se propagan:
package main
import "fmt"
func main() {
var a int
var ptr *int
var pptr **int
a = 3000
ptr = &a
pptr = &ptr
fmt.Printf("Antes: a = %d, *ptr = %d, **pptr = %d\n", a, *ptr, **pptr)
**pptr = 200 // Modifica el valor de 'a' a través de pptr
fmt.Printf("Después: a = %d, *ptr = %d, **pptr = %d\n", a, *ptr, **pptr)
}
Salida:
Antes: a = 3000, *ptr = 3000, **pptr = 3000
Después: a = 200, *ptr = 200, **pptr = 200
Esto demuestra cómo los punteros anidados pueden alterar el valor original de manera cascada, ofreciendo una forma potente de manipulación de memoria.
Reflexión sobre Punteros en Go
Muchos lenguajes compilados, como C/C++, ofrecen punteros "reales". Java, por otro lado, trabaja con "referencias", que son punteros que no permiten manipulación directa de direcciones ni aritmética de punteros. Go, siendo un lenguaje moderno, incluye el concepto de punteros de forma explícita.
La documentación oficial de Go especifica que todos los argumentos de función se pasan por valor. No existe el paso por referencia en el sentido tradicional. Para permitir la modificación de variables originales dentro de las funciones bajo el paradigma de paso por valor, Go implementa punteros.
Esta decisión de diseño subraya la estricta política de Go de pasar todo por valor. En lugar de implementar una "pasada por referencia" separada, Go utiliza punteros para lograr un efecto similar, manteniendo la coherencia del modelo de paso por valor. Esto puede mejorar el rendimiento y la eficiencia de la memoria en escenarios apropiados, similar a cómo Python maneja tipos de datos mutables.