Variables y Punteros en Go

Introducción

En Go, el manejo de variables y punteros es un concepto clave para comprender cómo los datos se pasan entre funciones y cómo se gestionan en memoria. Este documento tiene como objetivo proporcionar una explicación sencilla de estos conceptos, así como de las convenciones sobre cuándo pasar datos por valor y cuándo por referencia. También discutiremos los tipos primitivos, compuestos, y cómo funcionan los slices y maps.

1. Variables en Go

1.1 Tipos Primitivos

Los tipos primitivos en Go son los tipos de datos básicos como:

TipoRepresentaciónValor por DefectoEjemplo de Declaración
intint0var entero int
float64float640.0var flotante float64
boolboolfalsevar booleano bool
stringstring"" (cadena vacía)var cadena string

Estos tipos tienen un valor por defecto cuando se declaran pero no se inicializan. La siguiente tabla muestra los tipos y sus valores predeterminados:

Código de ejemplo:

package main

import "fmt"

func main() {
var entero int
var flotante float64
var booleano bool
var cadena string

fmt.Println("Valor por defecto de int:", entero) // output: 0
fmt.Println("Valor por defecto de float64:", flotante) // output: 0.0
fmt.Println("Valor por defecto de bool:", booleano) // output: false
fmt.Println("Valor por defecto de string:", cadena) // output: "" (cadena vacía)
}

1.2 Tipos Compuestos

Go también tiene tipos compuestos que permiten manejar estructuras de datos más complejas, como:

TipoRepresentaciónValor por DefectoEjemplo de Declaración
slice[]Tiponilvar items []string
mapmap[Key]Valuenilvar edades map[string]int
structstructCampos con valores por defecto de sus tipostype Persona struct { Nombre string; Edad int }

Los tipos compuestos como slices, maps, y structs permiten almacenar múltiples valores o agrupar varios campos de datos bajo un mismo nombre. Los valores por defecto de estos tipos suelen ser nil (es decir, no inicializados) o, en el caso de structs, los valores por defecto de los campos según su tipo.

Código de ejemplo:

package main

import "fmt"

type Persona struct {
Nombre string
Edad int
}

func main() {
var personas []string // Slice
var edades map[string]int // Map
var persona Persona // Struct

fmt.Println("Valor por defecto de un slice:", personas) // output: nil
fmt.Println("Valor por defecto de un map:", edades) // output: nil
fmt.Println("Valor por defecto de un struct:", persona) // output: { "" 0 }
}

2. Punteros en Go

Los punteros son una forma de almacenar la dirección de memoria de una variable en lugar de su valor. Esto permite que múltiples funciones o variables puedan acceder y modificar el mismo valor en memoria sin tener que copiarlo.

2.1 Declaración de un Puntero

Para declarar un puntero a un tipo de dato, se usa *Tipo, y para obtener la dirección de una variable se usa el operador &.

Código de ejemplo:

package main

import "fmt"

func main() {
var nombre string = "John"
var punteroNombre *string = &nombre // Almacena la dirección de nombre

fmt.Println("Valor de nombre:", nombre) // output: John
fmt.Println("Dirección de memoria de nombre:", &nombre) // Muestra la dirección en memoria
fmt.Println("Valor de punteroNombre:", punteroNombre) // Muestra la misma dirección
fmt.Println("Valor apuntado por punteroNombre:", *punteroNombre) // output: John
}

2.2 Modificación a través de Punteros

Los punteros permiten modificar el valor original de una variable desde otra parte del código.

Código de ejemplo:

package main

import "fmt"

func main() {
var nombre string = "John"
var punteroNombre *string = &nombre

*punteroNombre = "Doe" // Cambia el valor apuntado
fmt.Println("Nuevo valor de nombre:", nombre) // output: Doe
}

3. Convenciones de Go: ¿Cuándo pasar por Valor y Cuándo por Referencia?

3.1 Paso por Valor

Cuando pasas una variable por valor, estás creando una copia de la misma. Esto significa que cualquier cambio en la variable dentro de la función no afectará el valor original.

Usar paso por valor cuando:

  • El tamaño del dato es pequeño (ej. int, float64, bool).
  • No necesitas modificar el valor original.

Código de ejemplo:

func duplicar(n int) {
n = n * 2
}

func main() {
numero := 10
duplicar(numero) // Se pasa por valor
fmt.Println("Valor después de duplicar:", numero) // output: 10 (no se modificó)
}

3.2 Paso por Referencia (Puntero)

Cuando pasas una variable por referencia, pasas la dirección de memoria de esa variable, permitiendo modificar su valor original.

Usar paso por referencia cuando:

  • El dato es grande o complejo (ej. structs, slices grandes).
  • Necesitas modificar el valor original en una función.
  • Quieres evitar copiar grandes cantidades de datos.

Código de ejemplo:

func duplicar(n *int) {
*n = *n * 2
}

func main() {
numero := 10
duplicar(&numero) // Se pasa por referencia
fmt.Println("Valor después de duplicar:", numero) // output: 20 (se modificó)
}

4. Slices y Maps en Go

4.1 Slices

Los slices se pasan por valor, pero internamente contienen un puntero a los datos subyacentes. Esto significa que, aunque pases el slice por valor, los cambios en los datos del slice se reflejan fuera de la función.

Ejemplo de paso por valor en un slice:

func agregarItem(s []string) {
s[0] = "nuevoItem"
}

func main() {
items := []string{"item1", "item2"}
agregarItem(items)
fmt.Println("Slice después de modificar:", items) // output: [nuevoItem item2]
}

4.2 Maps

Al igual que los slices, los maps se pasan por valor, pero el valor que se pasa es un descriptor que contiene un puntero a los datos. Por lo tanto, las modificaciones dentro de la función afectan al map original.

Ejemplo de paso por valor en un map:

func modificarMap(m map[string]int) {
m["nuevo"] = 100
}

func main() {
miMap := map[string]int{"a": 1, "b": 2}
modificarMap(miMap)
fmt.Println("Map después de modificar:", miMap) // output: map[a:1 b:2 nuevo:100]
}

4.3 Paso por Referencia en Slices y Maps

En algunos casos, podrías querer modificar el descriptor de un slice o map, como cambiar su longitud o capacidad. Para esto, puedes pasar el slice o map como puntero.

Ejemplo con puntero:

func modificarSlice(s *[]string) {
*s = append(*s, "nuevoItem")
}

func main() {
items := []string{"item1", "item2"}
modificarSlice(&items)
fmt.Println("Slice después de modificar:", items) // output: [item1 item2 nuevoItem]
}

Conclusión

En Go, es crucial entender cuándo pasar variables por valor y cuándo por referencia. Para tipos primitivos como enteros y cadenas, el paso por valor es eficiente y adecuado cuando no necesitas modificar el valor original. Para tipos compuestos como slices y maps, aunque se pasan por valor, su naturaleza interna de punteros hace que las modificaciones afecten los datos originales. Sin embargo, en casos donde necesitas modificar el descriptor (tamaño o capacidad), es necesario pasar el slice o map como puntero.

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *