🐹 Biblia de Go (Golang)
Un recurso de alto rendimiento. Actualizado automáticamente por el Robot Gopher.
📚 Índice del Contenido
📂 Modulo B
-
Fundamentos
-
Estructuras_y_Composicion
-
Concurrencia_Nativa
🌐 Ver como Libro Web
👉 https://soylizardev.github.io/bibliadego/
Sintaxis Tipado y Operadores
Go es un lenguaje estáticamente tipado, lo que significa que el compilador sabe el tipo de cada variable. Sin embargo, su sintaxis es limpia y evita la verbosidad de lenguajes como Java.
1. Declaración de Variables
En Go existen tres formas principales de declarar variables:
A. Forma Estándar (con var)
Se usa principalmente para variables globales o cuando no vas a asignar un valor inmediatamente (inicializa con el Zero Value).
var nombre string = "Miguel"
var edad int // Se inicializa en 0
B. Declaración Corta (:=)
Es la más usada dentro de funciones. Go infiere el tipo automáticamente.
pais := "Venezuela" // Go sabe que es string
poblacion := 30000000 // Go sabe que es int
C. Constantes (const)
Valores que no cambian en el tiempo.
const Pi = 3.14159
2. Tipos de Datos Nativos
| Categoría | Tipos | Notas |
|---|---|---|
| Enteros | int, int8, int64, uint | int se ajusta a la arquitectura (32 o 64 bits). |
| Flotantes | float32, float64 | float64 es el estándar por precisión. |
| Booleanos | bool | true o false. |
| Cadenas | string | Inmutables y codificadas en UTF-8. |
3. Zero Values (Valores por Defecto)
Si declaras una variable y no le das valor, Go le asigna uno automáticamente. Esto evita errores de "puntero nulo" inesperados.
-
int / float:
0 -
string:
""(string vacío) -
bool:
false -
Punteros, Slices, Maps, Interfaces:
nil
4. Conversión de Tipos (Casting)
Go nunca hace conversión automática (ni siquiera de int a float64). Debes hacerlo explícitamente.
var a int = 10
var b float64 = float64(a) // Correcto
// var c float64 = a // Error de compilación
💡 Notas para el Backend
-
CamellCase: En Go, si una variable o función empieza con Mayúscula, es pública (exportada). Si empieza con minúscula, es privada del paquete.
-
Tipado fuerte: La rigidez de Go con los tipos es lo que lo hace tan seguro para sistemas de alto rendimiento; los errores se ven al compilar, no al ejecutar.
5. Operadores en Go
A. Aritméticos
Se usan para cálculos matemáticos básicos.
-
+(Suma) -
-(Resta) -
*(Multiplicación) -
/(División) -
%(Módulo/Resto)
B. Comparación
Devuelven un valor booleano (true o false).
-
==(Igual a) -
!=(Diferente de) -
<,>(Menor que, Mayor que) -
<=,>=(Menor o igual, Mayor o igual)
C. Lógicos
Para combinar condiciones.
-
&&(AND lógico) -
||(OR lógico) -
!(NOT lógico / Negación)
D. Incremento y Decremento
¡Cuidado aquí! A diferencia de otros lenguajes, en Go ++ y -- son sentencias, no expresiones. Esto significa que no puedes hacer y = x++. Solo puedes usarlos solos.
-
i++(Equivale ai = i + 1) -
i--(Equivale ai = i - 1)
E. Operadores de Asignación Compuesta
-
+=(Suma y asigna:x += 5) -
-=(Resta y asigna) -
*=,/=(Multiplica/Divide y asigna)
6. Precedencia de Operadores (Orden de ejecución)
Go sigue el orden matemático estándar, pero aquí tienes una tabla rápida de mayor a menor importancia:
-
*/%<<>>&&^ -
+-|^ -
==!=<<=>>= -
&& -
||
💡 Tip de Pro: Si tienes una expresión compleja, usa paréntesis
(). No solo aseguras el orden, sino que haces que tu código sea mucho más fácil de leer para otros programadores.
🧠 Resumen
-
Variables:
nombre := "Go"(Inferencia). -
Tipos: Estrictos (no mezcla
intconfloat64). -
Punteros:
¶ dirección,*para valor. -
Operadores:
++solo se usa al final de la variable, nunca antes.
🏷️ Custom Types (Tipos Personalizados)
Los tipos personalizados en Go permiten crear alias o tipos basados en tipos primitivos existentes (como string, int, float64).
¿Para qué sirven?
-
Claridad: El código es más legible (no es lo mismo un
stringque unIDUsuario). -
Seguridad: Evitan errores al restringir valores o tipos de datos específicos.
-
Poder: Permiten añadir métodos a tipos básicos (algo que veremos más adelante).
1. Definición Básica
Puedes crear un tipo a partir de cualquier tipo primitivo usando la palabra clave type.
// Definimos que 'edad' es un tipo basado en 'int'
type edad int
func main() {
var miEdad edad = 25
fmt.Println(miEdad) // Imprime: 25
}
2. Uso Práctico con Constantes (Enums a lo Go)
Esto es vital para estados de pedidos, roles de usuario, etc.
Ejemplo de "Kages":
// 1. Creamos el tipo base
type Kage string
// 2. Definimos las constantes válidas para ese tipo
const (
Hokage Kage = "Hokage"
Kazekage Kage = "Kazekage"
Mizukage Kage = "Mizukage"
)
func main() {
// Uso por declaración explícita
var lider Kage = Hokage
// Uso por inferencia de tipo
actualLider := Kazekage
fmt.Println("El líder actual es:", lider) //
}
💡 Ventajas Clave del Video
-
Restricción de valores: Si intentas asignar un valor que no esté definido en tus constantes (ej.
KageX), el compilador te ayudará a detectar que algo no está bien. -
Previsibilidad: Al usar estos tipos en funciones, te aseguras de que nadie pase un
stringgenérico cuando esperas un tipo de dato específico del dominio de tu negocio.
🔑Importancia
Este concepto es el puente hacia las Interfaces. En Go, a menudo creamos tipos personalizados para luego hacer que "cumplan" con una interfaz.
Generics
Los Generics nos permiten escribir funciones y estructuras de datos que pueden trabajar con múltiples tipos sin sacrificar la seguridad del tipado. Evitan la duplicación de código ("Don't Repeat Yourself" - DRY).
1. El Problema: Duplicación de Código
Antes de los genéricos, si querías una función para sumar, necesitabas una para cada tipo:
func sumarEnteros(a, b int) int { return a + b }
func sumarFloats(a, b float64) float64 { return a + b }
Esto es ineficiente porque la lógica es la misma, solo cambia el tipo.
2. La Solución: Funciones Genéricas
Con genéricos, declaras un parámetro de tipo (por convención se usa T) entre corchetes [] antes de los parámetros de la función.
Ejemplo de Función Sumar:
// T puede ser int o float64
func Sumar[T int | float64](a, b T) T {
return a + b
}
func main() {
// La misma función sirve para ambos casos
fmt.Println(Sumar(10, 20)) // Funciona con int
fmt.Println(Sumar(1.5, 2.5)) // Funciona con float64
}
3. Elementos Clave de la Sintaxis
-
Corchetes
[]: Aquí es donde defines las restricciones del genérico. -
Tipo
T: Es el nombre convencional del tipo genérico (puedes ponerle cualquier nombre, peroTes el estándar en Go). -
Constraints (Restricciones): El símbolo
|(pipe) actúa como un "O". En el ejemplo:int | float64significa que la función acepta cualquiera de los dos.
💡 Por qué es "Alto Rendimiento"
-
Seguridad en tiempo de compilación: A diferencia de usar
interface{}(que veremos luego), los genéricos no tienen penalización de rendimiento en ejecución porque el compilador genera el código específico para cada tipo que uses. -
Código Limpio: Reduces drásticamente la cantidad de líneas de código en proyectos grandes.
Salida Formateada
(fmt.Printf)
A diferencia de Println, Printf (Print Formatted) nos permite usar verbos (especificadores) para controlar exactamente cómo se ven los datos.
1. Verbos de Formato Comunes
Estos son los "comodines" que usas dentro del string para representar variables:
| Verbo | Descripción | Ejemplo de Uso |
|---|---|---|
%s | Strings (Cadenas de texto) | fmt.Printf("Hola %s", nombre) |
%d | Integers (Enteros en base 10) | fmt.Printf("Edad: %d", 25) |
%f | Floats (Números decimales) | fmt.Printf("Altura: %f", 1.80) |
%.2f | Floats con precisión (2 decimales) | fmt.Printf("%.2f", 3.14159) |
%t | Booleans (true o false) | fmt.Printf("Activo: %t", true) |
%v | Default Value (El valor "tal cual") | Útil cuando no sabes el tipo exacto |
%T | Type (Imprime el tipo de la variable) | fmt.Printf("Es un: %T", valor) |
2. Bases Numéricas y Conversiones
Go facilita mucho ver un número en diferentes sistemas desde la consola:
-
%b: Binario. -
%o: Octal. -
%x: Hexadecimal. -
%e: Notación científica.
numero := 255
fmt.Printf("Binario: %b\n", numero) // 11111111
fmt.Printf("Hex: %x\n", numero) // ff
3. Alineación y Tablas
Puedes definir el ancho de un campo para alinear texto a la izquierda o derecha, ideal para reportes en consola:
// %-10s -> Alinea string a la izquierda con 10 espacios
// %3d -> Alinea entero a la derecha con 3 espacios
fmt.Printf("%-10s | %3d\n", "Carlos", 25)
fmt.Printf("%-10s | %3d\n", "Ana", 30)
4. El "Truco" de fmt.Sprintf 🌟
Este es uno de los puntos más importantes del video. A veces no quieres imprimir en consola, sino guardar el texto formateado en una variable (para enviarlo por email, guardarlo en un log o devolverlo en una API).
// Sprintf NO imprime, DEVUELVE un string
mensaje := fmt.Sprintf("Usuario %s (ID: %d) creado.", "Miguel", 101)
fmt.Println(mensaje) // Ahora sí lo imprimes si quieres
💡 Tips
-
Salto de línea:
Printfno añade salto de línea al final. Debes poner siempre\nmanualmente para que la siguiente impresión no salga pegada. -
Preferencia: Usa
%.2fsiempre que manejes dinero o medidas para evitar que salgan 6 decimales por defecto.
Entrada de datos (Scan y Bufio)
En Go, pedir datos al usuario requiere entender cómo se comporta el Buffer (la memoria temporal donde se guarda lo que tecleas) y cómo el lenguaje accede a las variables mediante punteros.
1. La Familia fmt (Lectura básica)
A. fmt.Scan
Lee valores separados por espacios o saltos de línea. Es la forma más común de capturar números o palabras sueltas.
var edad int
fmt.Print("Ingresa tu edad: ")
// Se usa el símbolo & para que Scan sepa en qué dirección de memoria guardar el dato
fmt.Scan(&edad)
B. fmt.Scanln
Similar a Scan, pero termina la lectura inmediatamente al encontrar un salto de línea (cuando el usuario pulsa Enter). Es ideal para capturar varios datos que el usuario ingresa en una sola fila.
var dia, mes int
fmt.Print("Ingresa día y mes (ej: 15 03): ")
fmt.Scanln(&dia, &mes)
C. fmt.Scanf (Lectura con Formato)
Permite extraer datos que siguen un patrón o formato específico definido por nosotros, como si fuera una plantilla.
var hora, minuto int
fmt.Print("Ingresa la hora (HH:MM): ")
// El usuario debe escribir los dos puntos para que los datos se capturen correctamente
fmt.Scanf("%d:%d", &hora, &minuto)
2. Lectura Avanzada con bufio (Frases completas)
El paquete fmt deja de leer cuando encuentra el primer espacio. Si quieres capturar una frase completa (como un nombre con apellido), necesitas usar el paquete bufio junto con os.
import (
"bufio"
"os"
"strings"
)
// 1. Creamos un "Lector" que escucha la entrada estándar (teclado)
reader := bufio.NewReader(os.Stdin)
fmt.Print("Ingresa tu nombre completo: ")
// 2. Lee todo el texto incluyendo espacios hasta presionar Enter (\n)
texto, _ := reader.ReadString('\n')
// 3. Limpieza: ReadString guarda el texto con el "Enter" al final.
// Usamos strings.TrimSpace para quitar ese salto de línea y espacios extra
textoLimpio := strings.TrimSpace(texto)
3. El Problema del Buffer (Salto de Línea atrapado)
Un error común al mezclar fmt.Scan con bufio es que el programa parece "saltarse" una pregunta.
¿Por qué sucede?
Cuando usas fmt.Scan, el usuario escribe el dato y pulsa Enter. El dato se guarda en la variable, pero el salto de línea (\n) del Enter se queda flotando en el buffer. Si la siguiente instrucción es un ReadString, este verá ese "Enter" residual, pensará que ya terminaste de escribir y continuará sin dejarte teclear nada.
Solución: Limpiar el Buffer
Para evitar esto, después de usar un Scan y antes de un bufio, debemos limpiar el buffer leyendo ese salto de línea sobrante:
// Después de un fmt.Scan(&variable)
reader := bufio.NewReader(os.Stdin)
reader.ReadString('\n') // Esto "se come" el Enter sobrante y deja el buffer limpio
💡 Resumen de uso:
-
Para una sola palabra o número: Usa
fmt.Scan. -
Para datos con formato (ej. fechas 12/10): Usa
fmt.Scanf. -
Para frases con espacios: Usa
bufio.NewReader. -
Punteros: No olvides nunca el símbolo
&antes de la variable en la familiaScan.
Conversión de Datos (Paquete strconv)
Cuando capturamos datos con bufio, Go los recibe como un string. Para realizar operaciones matemáticas, debemos convertirlos a tipos numéricos (int, float, etc.) utilizando el paquete strconv.
1. Convertir String a Entero (ParseInt)
Es la función más completa para convertir texto en números enteros.
numero, err := strconv.ParseInt(texto, base, bitSize)
Parámetros técnicos:
-
s(string): El texto que quieres convertir (ej:"100"). -
base(int): El sistema numérico del texto.-
10: Decimal (el más usado). -
2: Binario. -
16: Hexadecimal. -
0: Autodetectar (según prefijos como0x).
-
-
bitSize(int): El tamaño del tipo de destino.-
0, 8, 16, 32, 64. Define si el número debe caber en unint8,int16, etc. -
Nota: Aunque definas un tamaño menor, la función siempre devuelve un tipo
int64.
-
2. La vía rápida: Atoi (S-to-i)
Si solo necesitas convertir un string a un entero común (int) en base 10, existe una función simplificada llamada Atoi (String to Integer).
texto := "25"
numero, err := strconv.Atoi(texto)
// Es el equivalente exacto a:
// strconv.ParseInt(texto, 10, 0)
3. Convertir String a Decimal (ParseFloat)
Para números con punto decimal, usamos ParseFloat.
texto := "3.1416"
// El segundo parámetro es el bitSize (32 o 64)
decimal, err := strconv.ParseFloat(texto, 64)
4. El camino inverso: Número a String
A veces necesitas convertir un número en texto (por ejemplo, para concatenarlo en un mensaje o guardarlo en un archivo).
De Entero a String (Itoa)
edad := 30
texto := strconv.Itoa(edad) // "Integer to ASCII"
De Decimal a String (FormatFloat)
Es más complejo porque requiere definir el formato y la precisión.
valor := 12.3456
// 'f' significa sin exponente, 2 es la cantidad de decimales, 64 es el tamaño
texto := strconv.FormatFloat(valor, 'f', 2, 64) // Resultado: "12.35"
💡 Tips:
[!IMPORTANT] Manejo de Tipos: Recuerda que
ParseIntsiempre devuelve unint64. Si tu función espera unintnormal, deberás convertirlo manualmente después de la función:resultadoFinal := int(numeroConvertido)
[!WARNING] Limpieza previa: Antes de usar
strconv, asegúrate de usarstrings.TrimSpace(texto)para eliminar saltos de línea (\n) o espacios accidentales, de lo contrario la conversión fallará con un error.
Estructuras de control
Las estructuras de control permiten dirigir el flujo de ejecución. En Go, la sintaxis es limpia: no se usan paréntesis en las condiciones, pero las llaves { } son obligatorias.
1. Condicionales (Toma de Decisiones)
A. Sentencia if / else
Go permite una sintaxis especial llamada "if con sentencia corta", donde declaras una variable que solo existe dentro del bloque del if.
// Sintaxis estándar
if edad >= 18 {
fmt.Println("Mayor de edad")
} else {
fmt.Println("Menor de edad")
}
// Sentencia corta (Muy usada en manejo de errores)
if valor := calcular(); valor > 100 {
fmt.Println("Valor alto:", valor)
}
// 'valor' no existe aquí afuera, liberando memoria automáticamente
B. El potente switch
En Go, el switch es más flexible que en Java. No necesitas break (es automático) y puedes evaluar condiciones.
// Switch con valor fijo
dia := "Lunes"
switch dia {
case "Lunes", "Martes", "Miercoles":
fmt.Println("Día de trabajo")
case "Sabado", "Domingo":
fmt.Println("Descanso")
default:
fmt.Println("Día no válido")
}
// Switch sin condición (funciona como un if/else largo más limpio)
numero := 15
switch {
case numero < 0:
fmt.Println("Negativo")
case numero > 0 && numero < 100:
fmt.Println("Positivo pequeño")
default:
fmt.Println("Número grande")
}
2. Bucles e Iteración (El Bucle Único)
En Go solo existe el for. No hay while ni do-while, pero el for se adapta para cumplir esas funciones.
A. For clásico (Tres componentes)
for i := 0; i < 5; i++ {
fmt.Println("Iteración:", i)
}
B. For como "While" (Solo condición)
contador := 0
for contador < 5 {
fmt.Println(contador)
contador++
}
C. Bucle Infinito
Ideal para servidores que deben estar siempre escuchando peticiones.
for {
// Código que corre por siempre
if detener {
break // Única forma de salir
}
}
D. For Range (Iterar colecciones)
Es la forma de oro para recorrer Slices y Maps.
nombres := []string{"Miguel", "Ana", "Jose"}
// Retorna índice y valor
for i, nombre := range nombres {
fmt.Printf("%d - %s\n", i, nombre)
}
// Si no necesitas el índice, usa el blank identifier (_)
for _, nombre := range nombres {
fmt.Println(nombre)
}
💡 Tips de Estilo Go
-
Evita el "Else" innecesario: Si un
iftermina en unreturn, no pongas unelse. Mantén el código "plano" hacia la izquierda. -
Switch sobre If: Si tienes más de 3 condiciones sobre la misma variable, usa un
switch; es mucho más legible.
Punteros y Memoria
1. ¿Qué es un Puntero?
Normalmente, una variable guarda un valor (como el número 25). Un puntero, en cambio, guarda la dirección de memoria donde está ese valor.
-
&(Operador de dirección): Se usa para obtener la dirección de una variable. ("¿Dónde vive?") -
*(Operador de desreferencia): Se usa para ver o cambiar el valor que hay en esa dirección. ("¿Qué hay dentro?")
nombre := "Miguel"
puntero := &nombre // 'puntero' guarda la dirección de memoria de 'nombre'
fmt.Println(puntero) // Imprime algo como: 0xc000010250
fmt.Println(*puntero) // Imprime: "Miguel" (el valor al que apunta)
2. Paso por Valor vs. Paso por Referencia
Aquí es donde está el truco para el Alto Rendimiento:
A. Paso por Valor (Copia)
Cuando pasas una variable a una función, Go crea una copia nueva. Si la función cambia el valor, la variable original no cambia.
- Se usa en:
int,float,string,booly Arrays.
func cambiarValor(n int) {
n = 100 // Esto solo cambia la copia
}
B. Paso por Referencia (Puntero)
Si pasas el puntero (*int), la función recibe la dirección real. Si la función cambia el valor, la variable original sí cambia.
- Ventaja de rendimiento: Si tienes un objeto gigante (como un archivo de 1GB), no lo copias; solo pasas la dirección (unos pocos bytes).
func cambiarReal(n *int) {
*n = 100 // Esto cambia el valor original en memoria
}
| Tipo | Comportamiento | Rendimiento |
|---|---|---|
Valor (int, Array) | Copia todo el dato. | Lento si el dato es grande. |
Referencia (Punteros) | Copia solo la dirección. | Rápido (siempre pesa lo mismo). |
Híbridos (Slices, Maps) | Se comportan como referencia. | Muy eficiente. |
🧠 El "Mindset" de Go:
A diferencia de otros lenguajes, en Go usamos punteros principalmente por dos razones:
-
Para que una función pueda modificar un dato original.
-
Por eficiencia, para no copiar estructuras de datos muy grandes en memoria.
Resumen
Punteros en 1 frase: Es una variable que guarda la dirección de memoria de OTRA variable.
-
&variable-> "Dime la dirección de esta variable" (Dirección). -
*puntero-> "Dime qué hay dentro de esta dirección" (Valor).
¿Por qué en Backend? Para modificar datos originales dentro de funciones sin crear copias pesadas en la RAM.
Arrays, Slices y Maps
Esta nota cubre los fundamentos de la gestión de colecciones de datos en Go, enfocada en el Módulo B.1 de nuestra Biblia de Estudio.
SECCIÓN 1: ARRAYS (Arreglos Estáticos)
Definición: Tienen un tamaño fijo que no puede cambiar. Se usan para listas donde sabes exactamente cuántos elementos habrá (ej. días de la semana).
Declaración y creación:
- var miArray [3]int //Crea un array de 3 enteros, todos en 0
- numeros := [3]int{10, 20, 30} //Crea un array con valores iniciales
Operaciones:
-
Leer:
dato := numeros[0] -
Modificar:
numeros[1] = 50 -
Longitud:
len(numeros)
SECCIÓN 2: SLICES (Arreglos Dinámicos)
Definición: Son punteros a un array. Pueden crecer y son mucho más eficientes para el Backend. Tienen "Pointer", "Len" (longitud) y "Cap" (capacidad).
Formas de crear un Slice:
-
Var (Nil):
var lista []string(No ocupa memoria todavía) -
Literal:
lista := []string{"A", "B"} -
Make (Alto Rendimiento):
lista := make([]string, 0, 10)(Crea un slice de tamaño 0 pero reserva espacio para 10 elementos de una vez).
Operaciones:
-
Agregar (Create):
lista = append(lista, "Nuevo Dato") -
Unir dos slices:
lista = append(lista, otraLista...) -
Leer/Modificar: Igual que el array:
lista[0] = "Editado" -
Borrar un elemento (índice i):
lista = append(lista[:i], lista[i+1:]...)
SECCIÓN 3: MAPS (Diccionarios Clave-Valor)
Definición: Almacenan datos usando una clave única (como un ID o un nombre). Son ultra rápidos para buscar información.
Formas de crear un Map:
-
Make:
miMap := make(map[string]int) -
Literal:
edades := map[string]int{"Miguel": 25, "Jose": 30}
Operaciones:
-
Agregar o Modificar:
miMap["Clave"] = 100 -
Borrar:
delete(miMap, "Clave") -
Lectura Segura (Comma OK):
valor, ok := mapa[clave] if ok { // La clave existe, usar 'valor' } else { // La clave NO existe }
cortar---
SECCIÓN 4: NOTAS DE RENDIMIENTO (Para tu Roadmap)
-
Pre-asignar: Siempre que uses
make, trata de ponerle una capacidad. Si no lo haces, Go tiene que crear nuevos arrays internos cada vez que el slice se llena, y eso consume CPU. -
Copia de datos: Los Arrays se copian enteros (lento). Los Slices y Maps solo copian su "encabezado" (rápido). Por eso en Backend casi siempre usamos Slices.
Funciones y retornos Multiples
En Go, las funciones son "ciudadanos de primera clase" (First-class citizens). Esto significa que pueden ser asignadas a variables, pasadas como argumentos y retornadas por otras funciones.
1. Anatomía de una Función
A diferencia de Java, el tipo de dato va después del nombre del parámetro.
func sumar(a int, b int) int {
return a + b
}
2. El Superpoder: Retornos Múltiples
Esta es la característica más icónica de Go. Se usa casi siempre para devolver un resultado y un error al mismo tiempo.
func dividir(dividendo, divisor float64) (float64, error) {
if divisor == 0 {
return 0, fmt.Errorf("no puedes dividir por cero")
}
return dividendo / divisor, nil
}
func main() {
res, err := dividir(10, 2)
if err != nil {
fmt.Println("Error:", err)
return
}
fmt.Println("Resultado:", res)
}
[!IMPORTANT] En Go, no usamos
try-catch. Usamos retornos múltiples para obligar al programador a lidiar con el error en el momento.
3. Retornos Nombrados (Named Returns)
Puedes pre-declarar las variables que vas a retornar en la firma de la función. Esto hace que el código sea más legible en funciones cortas.
Go
func rectangulo(largo, ancho float64) (area, perimetro float64) {
area = largo * ancho
perimetro = (largo + ancho) * 2
return // Retorna automáticamente area y perimetro
}
4. Funciones Variádicas
Son funciones que aceptan un número indefinido de argumentos (como fmt.Println). Se usa el prefijo ...
func sumarTodo(numeros ...int) int {
total := 0
for _, n := range numeros {
total += n
}
return total
}
5. Funciones Anónimas y Closures
Puedes declarar una función sin nombre dentro de otra y ejecutarla al instante o guardarla en una variable.
func main() {
saludo := func(nombre string) {
fmt.Printf("Hola %s desde una función anónima\n", nombre)
}
saludo("Miguel")
}
💡 Conexión con las Interfaces1
-
Una interfaz define firmas de funciones.
-
Si una función de interfaz dice que devuelve
(string, error), tu struct debe tener un método que devuelva exactamente eso.
Ver [[04_Interfaces|Interfaces]] para mayor información
Manejo de errores y Control de Flujo
En Go, el manejo de errores es explícito. No existen las excepciones (try-catch) como en Java. Esto nos obliga a tratar los errores como parte de la lógica de negocio, lo que hace que el software de backend sea extremadamente robusto y predecible.
1. El Patrón Estándar: if err != nil
Las funciones que pueden fallar siempre devuelven un tipo error como último valor de retorno.
func buscarUsuario(id int) (User, error) {
// ... lógica de búsqueda
if noEncontrado {
return User{}, fmt.Errorf("usuario con id %d no existe", id)
}
return usuario, nil // nil significa que no hubo error
}
// Uso profesional:
user, err := buscarUsuario(10)
if err != nil {
// Aquí decides qué hacer: log, retornar el error o morir
fmt.Println("Error encontrado:", err)
return
}
fmt.Println("Usuario encontrado:", user.Nombre)
2. defer: El "Limpiador" Automático
La palabra clave defer asegura que una función se ejecute justo antes de salir de la función actual. Es vital para la limpieza de recursos y evitar fugas de memoria (memory leaks).
-
Uso común: Cerrar archivos, cerrar conexiones a DB, liberar bloqueos (Mutex).
-
Orden de ejecución: Si hay varios
defer, se ejecutan en orden LIFO (Last In, First Out), como una pila de platos.
f, err := os.Open("config.json")
if err != nil {
return err
}
defer f.Close() // Se ejecutará al final de la función, pase lo que pase
3. Errores Críticos: panic y recover
-
panic: Detiene la ejecución normal del programa inmediatamente. Solo se debe usar en errores irrecuperables (ej: no se pudo cargar la variable de entorno de la DB al arrancar). -
recover: Solo funciona dentro de una función llamada pordefer. Permite capturar unpanicpara que el programa no "explote" y pueda seguir vivo.
func rescatar() {
if r := recover(); r != nil {
fmt.Println("Recuperado de un error crítico:", r)
}
}
func miFuncion() {
defer rescatar()
panic("¡Conexión perdida con el satélite!")
}
4. Mejores Prácticas para tu Roadmap
-
No ignores los errores: Nunca uses
_ , _ = funcion(). Siempre verifica elerr. -
Los errores son valores: Puedes compararlos y envolverlos (
fmt.Errorf("... %w", err)) para dar más contexto. -
Defer temprano: Pon el
deferinmediatamente después de comprobar que el recurso se abrió correctamente.
Structs y Methods
En Go no hay clases. Usamos Structs para agrupar datos y Methods para añadirles comportamiento.
1. Definición de un Struct
Un struct es una colección de campos.
type Usuario struct {
ID int
Name string
Email string
Activo bool
}
2. Instanciación (Crear un objeto)
Instanciar no es más que crear una variable basada en el molde (el struct).
Hay dos formas de hacerlo y una regla de oro:
-
Por posición (Rápida pero peligrosa):
-
u := Usuario{1, "Miguel", "migue@mail.com"} // Tienes que poner los datos en el orden exacto en que están en el struct. // Si mañana cambias el orden en el struct, esto rompe. -
Por nombre de campo (La recomendada):
u := Usuario{ Name: "Miguel", Email: "migue@mail.com", ID: 1, } // No importa el orden, es mucho más legible y si el struct cambia, esto no rompe.
3. Methods (El "Comportamiento")
Los métodos se asocian a un struct mediante un Receiver (receptor).
3.1 El "Receiver" (Receptor)
En otros lenguajes, las funciones viven dentro de la clase. En Go, las funciones están "sueltas", y tú las "pegas" a un struct mediante el Receiver.
Imagina que el Receiver es como decirle a la función: "Tú solo le perteneces a este tipo de dato".
A. Receiver por Valor (Copia)
Receiver por Valor (u Usuario): Go saca una fotocopia del usuario. La función trabaja con la copia. Si cambias el nombre ahí dentro, el original sigue igual. (Úsalo para leer datos).
func (u Usuario) Presentarse() string {
return "Hola, soy " + u.Name
}
B. Receiver por Puntero (Referencia) ⚡
CLAVE PARA EL RENDIMIENTO: Se usa para modificar el struct original o para evitar copiar structs muy grandes en memoria.
func (u *Usuario) Desactivar() {
u.Activo = false // Modifica el original directamente
}
💡 Mindset de Backend: Composición sobre Herencia
En Go no existe extends. Si quieres que un Admin tenga los campos de Usuario, usamos Embedding (Incrustación).
type Admin struct {
Usuario // El Admin ahora "tiene" ID, Name, Email, etc.
Nivel int
}
Embedding (Incrustación / "Herencia" a lo Go)
En Java, dirías: class Admin extends Usuario. En Go, decimos: "El Admin tiene un Usuario adentro".
El Embedding es poner un struct dentro de otro sin darle un nombre de campo. Al hacerlo, el struct de afuera "hereda" automáticamente todos los campos y métodos del de adentro.
type Usuario struct {
Nombre string
}
type Admin struct {
Usuario // <--- Esto es EMBEDDING (no tiene nombre de campo)
Nivel int
}
// Ahora puedes hacer esto:
var a Admin
a.Nombre = "Miguel" // Accedes directo, como si Nombre fuera del Admin
¿Por qué es mejor que la herencia? Porque es más simple. No hay jerarquías complicadas de "padres e hijos"; simplemente compones piezas pequeñas para armar una grande.
Ver [[05_Embedding vs Herencia|Embedding vs Herencia]] para profundizar más en el tema.
Comparativa Java vs Go
| Característica | Java | Go |
|---|---|---|
| Contenedor | class | struct |
| Comportamiento | Métodos dentro de la clase | Métodos fuera, unidos por un Receiver |
| Herencia | extends | Embedding (Incrustación de structs) |
| Polimorfismo | Herencia de clases | Interfaces (Basadas en comportamiento) |
Ejercicio:
🛠️ Reto: Gestión de Productos
Objetivo: Crear un programa que gestione el precio de un producto usando métodos con diferentes tipos de receivers.
1. Definir la Estructura (El Molde)
Crea un struct llamado Producto que tenga:
-
Nombre(string) -
Precio(float64)
2. Crear los Métodos (El Comportamiento)
-
Método
Mostrar(): Debe tener un Receiver por Valor. Su única función es imprimir en consola: ```"Producto: [Nombre] - Precio: $[Precio]". -
Método
AplicarDescuento(): Debe recibir unporcentaje(float64) y tener un Receiver por Puntero. Debe calcular el nuevo precio y actualizarlo en el struct original.
3. Ejecución en el main
-
Instancia un producto usando la forma explícita (nombres de campos). Ejemplo:
p := Producto{Nombre: "Laptop", Precio: 1000}. -
Llama al método
Mostrar()para ver el precio inicial. -
Llama al método
AplicarDescuento(10)(para un 10% de descuento). -
Llama de nuevo a
Mostrar()para comprobar que el precio bajó de verdad.
💡 Pista para el éxito:
Si el método AplicarDescuento no tuviera el asterisco * en el receiver, cuando vuelvas a llamar a Mostrar(), verías el precio viejo. El * es la llave que le permite a la función entrar a la memoria y cambiar el dato original.
Respuesta
package main
import "fmt"
type producto struct {
nombre string
precio float64
}
func (p producto) mostrar() {
fmt.Printf("Producto: %s - Precio: $%f\n", p.nombre, p.precio)
}
func (p *producto) AplicarDescuento(porcentaje float64) {
p.precio= p.precio - p.precio * (porcentaje /100)
}
func main() {
p := producto {
nombre: "laptop",
precio: 1000,
}
p.mostrar()
p.AplicarDescuento(10)
p.mostrar()
}
Interfaces
Las interfaces nos permiten escribir código más flexible y re-utilizable sin depender de tipos en específicos. En Go, las Interfaces definen el comportamiento de un objeto. A diferencia de Java, no usas la palabra implements. Si un struct tiene los métodos que pide la interfaz, la implementa automáticamente (Duck Typing).
"Si camina como pato y grazna como pato, entonces es un pato".
1. Definición de una Interfaz
Una interfaz solo contiene las firmas de los métodos (nombre, parámetros y retorno).
type Mensajero interface {
Enviar(mensaje string) error
}
2. Implementación Implícita
No necesitas decir nada en el struct. Solo crea el método con el mismo nombre y firma.
type Email struct {
Direccion string
}
// Email implementa Mensajero automáticamente por tener este método
func (e Email) Enviar(m string) error {
fmt.Printf("Enviando Email a %s: %s\n", e.Direccion, m)
return nil
}
type SMS struct {
Numero string
}
// SMS también implementa Mensajero
func (s SMS) Enviar(m string) error {
fmt.Printf("Enviando SMS al %s: %s\n", s.Numero, m)
return nil
}
3. El Poder del Polimorfismo
Puedes crear funciones que reciban la interfaz, y así aceptarán cualquier struct que la cumpla. Esto permite desacoplar el código.
func Notificar(m Mensajero, texto string) {
m.Enviar(texto)
}
func main() {
e := Email{Direccion: "miguel@go.dev"}
s := SMS{Numero: "+58412000"}
// La función acepta ambos porque ambos son 'Mensajeros'
Notificar(e, "¡Hola desde Go!")
Notificar(s, "¡Hola desde Go!")
}
4. La Interfaz Vacía (interface{} o any)
Una interfaz sin métodos es cumplida por absolutamente todo. Es el equivalente a Object en Java, pero en Go moderno (1.18+) usamos el alias any.
func Describir(i any) {
fmt.Printf("Valor: %v, Tipo: %T\n", i, i)
}
⚠️ Nota: No abuses de any, pierde la seguridad del tipado.
💡 Diferencias Clave con Java
| Característica | Java | Go |
|---|---|---|
| Vínculo | Explícito (implements) | Implícito (Automático) |
| Contrato | Rígido | Flexible (Desacoplado) |
| Diseño | Interfaces grandes | Interfaces pequeñas (1 o 2 métodos) |
Embedding vs Herencia
En Go, aunque puedes hacer cosas que parecen POO, el paradigma se define oficialmente como Programación Orientada a la Composición (o simplemente un lenguaje basado en tipos y composición).
En Go, el concepto de Herencia (propio de Java/C++) no existe. En su lugar, utilizamos Embedding (Incrustación), que es la implementación práctica del principio: "Prefiere la composición sobre la herencia".
1. La Filosofía
-
Java (Herencia): Relación "Es un" (is-a). Un
Gatoes unAnimal. -
Go (Composición): Relación "Tiene un" (has-a). Un
Gatotiene características deAnimal.
2. Cómo funciona el Embedding
Para "heredar" campos y métodos de otro struct, simplemente lo ponemos dentro de nuestro nuevo struct sin darle un nombre de campo.
type Persona struct {
Nombre string
Edad int
}
func (p Persona) Saludar() {
fmt.Printf("Hola, soy %s\n", p.Nombre)
}
type Empleado struct {
Persona // <--- Esto es Embedding (Campo anónimo)
Sueldo float64
}
func main() {
e := Empleado{
Persona: Persona{Nombre: "Miguel", Edad: 25},
Sueldo: 1500.0,
}
// ¡Magia! Empleado puede usar los campos y métodos de Persona directamente
fmt.Println(e.Nombre)
e.Saludar()
}
3. Promoción de Métodos
Cuando haces embedding, los métodos del struct interno se promueven al struct externo. Es decir, Empleado gana automáticamente el método Saludar().
4. Sobrescritura (Shadowing)
Si el struct externo define un método con el mismo nombre que el interno, Go usará el del externo. Esto es lo más parecido al @Override de Java.
func (e Empleado) Saludar() {
fmt.Printf("Hola, soy el empleado %s y gano %.2f\n", e.Nombre, e.Sueldo)
}
5. El problema de la Ambigüedad
Go permite "herencia múltiple" de facto mediante composición, pero si dos structs embebidos tienen el mismo método, Go se detiene y pide que tú definas cuál usar.
type AllRounder struct {
Striker // Tiene Pelear()
Grappler // Tiene Pelear()
}
// SOLUCIÓN: Definir Pelear en AllRounder para quitar la ambigüedad
func (a AllRounder) Pelear() {
a.Striker.Pelear() // Decides usar el de Striker
}
💡 Cuadro Comparativo Herencia vs Embedding
| Característica | Herencia (Java) | Composición/Embedding (Go) |
|---|---|---|
| Jerarquía | Vertical (Padres/Hijos) | Horizontal (Módulos/Piezas) |
| Acoplamiento | Fuerte (Frágil) | Débil (Flexible) |
| Múltiple | No permitida (Normalmente) | Permitida y común |
| En tiempo de... | Compilación (Estática) | Compilación (Pero más dinámica) |
📝 Términos que debes conocer:
-
Composición sobre Herencia (Composition over Inheritance): Es el mantra de Go. En lugar de una jerarquía vertical (padre-hijo), usamos una estructura horizontal donde un struct contiene a otros para ganar sus habilidades.
-
Duck Typing (Tipado de Pato): Es el nombre del sistema de interfaces de Go. "Si camina como pato y grazna como pato, lo trato como un pato". No importa el origen del struct, solo que tenga el método necesario.
-
Polimorfismo Ad-hoc: El polimorfismo en Go es mucho más dinámico porque puedes hacer que un struct sea compatible con una interfaz en cualquier momento, solo añadiendo el método, sin tocar la definición original del struct.
💡 Un dato curioso
El creador de Go, Rob Pike, dice que Go es un lenguaje Post-POO. Tomaron lo bueno de los objetos (encapsulamiento y métodos) y quitaron lo malo (la herencia compleja que a veces hace que el código sea un desastre de mantener).