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:
| Tipo | Representación | Valor por Defecto | Ejemplo de Declaración |
|---|---|---|---|
int | int | 0 | var entero int |
float64 | float64 | 0.0 | var flotante float64 |
bool | bool | false | var booleano bool |
string | string | "" (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:
| Tipo | Representación | Valor por Defecto | Ejemplo de Declaración |
|---|---|---|---|
slice | []Tipo | nil | var items []string |
map | map[Key]Value | nil | var edades map[string]int |
struct | struct | Campos con valores por defecto de sus tipos | type 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.