# Go desde Cero, Pero Bien: Anatomía de un Programa Go, Errores y Slices
Tabla de Contenidos
🧩 Módulo 1, Capítulo 2: La Anatomía de un Programa Go
Bueno muchachones, ya controlamos las variables como unidades de información. Ahora vamos a ensamblarlas para crear un programa funcional. Veremos cómo se estructura, cómo respira y, lo más importante, cómo maneja los errores, esos problemitas que joden!!!, que es muy diferente a como lo hacen otros lenguajes.
El Punto de Entrada: package main y func main()
Todo programa ejecutable en Go, ya sea un microservicio o una herramienta de línea de comandos (CLI), tiene un punto de partida idéntico y no negociable mis bros.
package main
import "fmt" // paquete que nos permite hacer el simil del console.log en Js
func main() { fmt.Println("La ejecución comienza aquí.")}package main: Esta no es una línea cualquiera. Es una directiva para el compilador de Go. Le dice: “Tu objetivo
final es producir un archivo ejecutable, no una librería para que otros la usen”. Si el paquete se llamara utils o
database, el compilador generaría una librería (.a), pero main es la palabra clave para crear un binario.
func main(): Dentro del paquete main, esta función es la puerta de entrada. Cuando ejecutas tu programa
compilado (./mi-programa), el runtime de Go (el entorno que gestiona la memoria, la concurrencia, etc.) se
inicializa y luego cede el control a la primera línea de código de tu func main(). Solo puede haber una func main en
todo el programa.
Análisis Quirúrgico: Por qué esta rigidez? Simplicidad y predecibilidad. No hay que buscar en archivos de
configuración ni heredar de clases complejas para saber dónde empieza todo. Si es un ejecutable, empieza en main.main.
Punto.!!! Esta convención elimina la sobrecarga cognitiva y facilita la lectura de cualquier proyecto Go.
El Tipo error: Manejando Problemas como un Profesional
Go toma una postura radicalmente diferente a la de lenguajes como Java, Python o C# en el manejo de errores. En Go, un error no es una excepción, es un valor.
El Problema que Resuelve: Las excepciones (try/catch/finally) rompen el flujo normal de ejecución. El control
salta desde donde ocurre el error hasta un bloque catch que puede estar muy lejos, haciendo difícil seguir la lógica
del programa. Además, es fácil olvidar capturar una excepción, lo que puede hacer que el programa se rompa
inesperadamente :S
(cuando comenzaba cuantos dolores de cabeza me dio mi querido Java hahaha)
La Solución Elegante de Go: Bueno, en Go, las funciones que pueden fallar devuelven su resultado normal junto con un
segundo valor de tipo error. Si la operación fue exitosa, el error es nil (nulo). Si algo salió mal, el error
contiene la información del problema.
import ("fmt""strconv")
func main() {// strconv.Atoi convierte un string a un entero. Puede fallar.// Por eso devuelve dos valores: el resultado (int) y un posible error (error).numero, err := strconv.Atoi("123")if err != nil {// Si err NO es nil, se jodio.!!! Lo manejamos inmediatamente.fmt.Println("Error: no se pudo convertir el string.", err)return // Salimos del programa de forma controlada.}
// Si llegamos aquí, es porque err fue nil. El programa sigue su flujo normal.fmt.Println("El número es:", numero)}Cómo Funciona por Debajo: error no es un tipo mágico. Es una interfaz predefinida (Tranquis mas adelante les
cuento bien este tema de las interfaces) muy simple:
type error interface {Error() string}Cualquier tipo de dato que tenga un método llamado Error() que devuelva un string cumple con la interfaz error y,
por lo tanto, puede ser usado como un valor de error. Esto es increíblemente poderoso, porque te permite crear tus
propios tipos de error personalizados con mucha más información (códigos de error, contexto, etc.), manteniendo la misma
interfaz estándar.
Este patrón if err != nil (condiciones hahaha aun lo las vemos pero eso viene!!! hahaha) te obliga a confrontar los
errores en el momento en que ocurren, haciendo el código infinitamente más robusto y legible.
Ciclos e Iteraciones: for es tu Único Amigo
A diferencia de otros lenguajes que ofrecen for, while, do-while, etc., Go simplifica drásticamente las cosas: *
*solo tiene una palabra clave para los ciclos: for**. Pero no te equivoques, este for es extremadamente versátil y
puede emular el comportamiento de todos los demás tipos de bucles.
El for Básico (como for en C/Java)
Este es el tipo de bucle más común cuando necesitas un contador o un número específico de iteraciones.
func main() {fmt.Println("--- Bucle For Básico ---")for i := 0; i < 5; i++ { // Inicialización; Condición; Post-sentenciafmt.Println("Iteración número:", i)}}Anatomía del for básico:
-
Inicialización (
i := 0): Se ejecuta una sola vez al principio del bucle. Aquí declaramos e inicializamos nuestra variable de controli. -
Condición (
i < 5): Se evalúa antes de cada iteración. Si la condición estrue, el cuerpo del bucle se ejecuta. Si esfalse, el bucle termina. -
Post-sentencia (
i++): Se ejecuta al final de cada iteración, después de que el cuerpo del bucle se haya completado. Comúnmente se usa para incrementar o decrementar el contador.
Cómo Funciona por Debajo: El compilador de Go transforma esta estructura en una serie de saltos condicionales y
asignaciones de memoria para la variable i. No hay magia aquí, es una forma muy eficiente de controlar el flujo de
ejecución.
El for como while
Si omites la inicialización y la post-sentencia, el for actúa como un bucle while, repitiéndose mientras una
condición sea verdadera.
func main() {fmt.Println("\n--- Bucle For como While ---")sum := 1for sum < 10 { // Solo la condiciónsum += sumfmt.Println("La suma actual es:", sum)}}Este tipo de bucle es ideal cuando no sabes de antemano cuántas veces necesitas iterar, pero sí sabes cuándo debe terminar basado en una condición.
El for Infinito
Si omites la condición (y por ende, la inicialización y post-sentencia), creas un bucle infinito. Pilas con este pana!!!
Necesitarás una sentencia break dentro del bucle para salir de él, o tu programa nunca terminará.
func main() {fmt.Println("\n--- Bucle For Infinito (con break) ---")contador := 0for { // Bucle infinitofmt.Println("Contando:", contador)contador++if contador >= 3 {break // Salimos del bucle cuando contador llega a 3}}}El for range (para colecciones, el caballo de batalla!!!)
Este es el bucle más idiomático de Go para iterar sobre colecciones como slices, arrays, mapas o cadenas de texto. Lo veremos más en profundidad cuando hablemos de slices y arrays.
func main() {fmt.Println("\n--- Bucle For Range (ejemplo previo) ---")nombres := []string{"Alice", "Bob", "Charlie"}for index, value := range nombres {fmt.Printf("Índice: %d, Valor: %s\n", index, value)}}El for range te devuelve dos valores en cada iteración: el índice (o clave) y el valor del elemento. Si solo
necesitas uno de ellos, puedes usar el guion bajo _ para descartar el otro.
Por qué la simplicidad del for es una fortaleza: Elimina la confusión y la necesidad de elegir entre múltiples
constructos de bucle. El único for es lo suficientemente flexible para cubrir todos los escenarios de iteración, lo
que contribuye a la filosofía de simplicidad y claridad de Go. Cuando tienes tantas opciones hahaha ya ni sabes cual
usar :P
Arrays: La Base Rígida
Un array es una colección de elementos del mismo tipo con un tamaño fijo definido en la compilación.
var miArray [3]int // Declara un array de 3 enteros.miArray[0] = 10miArray[1] = 20miArray[2] = 30Cómo Funciona por Debajo:
-
Memoria Contigua: Cuando declaras
[3]int, el compilador reserva un bloque de memoria continuo y sin huecos lo suficientemente grande para albergar 3 enteros. Si unintocupa 8 bytes, el array ocupará 24 bytes consecutivos. Esta asignación contigua es lo que hace que el acceso a elementos por índice sea extremadamente rápido. -
Valor, no Referencia: Esta es la clave. El tipo de un array incluye su tamaño.
[3]inty[4]intson tipos diferentes e incompatibles. Cuando pasas un array a una función, el programa **copia el array entero, valor por valor **.
func modificarArray(arr [3]int) {arr[0] = 999 // Modificamos la COPIA.}
func main() {miArray := [3]int{10, 20, 30}modificarArray(miArray)fmt.Println(miArray[0]) // Imprime 10, no 999. El original no se tocó.}Esto garantiza aislamiento, pero puede ser muy ineficiente si el array es grande, ya que cada copia implica copiar todos los bytes del array. Por esta razón, en la práctica, los arrays se usan poco directamente. Son el cimiento sobre el que se construye algo mucho más poderoso: los slices.
Slices: La Flexibilidad Dinámica (Lo que seguro usaras mas que el pan)
Un slice es una “vista” o “ventana” flexible y ligera sobre una porción de un array. Los slices son el tipo de colección más usado en Go con diferencia.
// Crear un slice a partir de un literal (esto crea un array subyacente anónimo)miSlice := []int{10, 20, 30}Cómo Funciona por Debajo:
Un slice no almacena los datos directamente. Es una pequeña estructura (un header) que contiene tres piezas de información:
-
Puntero (
ptr): Una dirección de memoria que apunta al primer elemento del array subyacente que el slice puede ver. -
Longitud (
len): El número de elementos que contiene el slice. -
Capacidad (
cap): El número de elementos desde el inicio del slice hasta el final del array subyacente.
Cuando pasas un slice a una función, solo estás copiando esta pequeña estructura de 3 campos, no todos los datos del array. Por eso es tan eficiente. Y como el puntero apunta al mismo array subyacente, los cambios se reflejan en todas partes.
func modificarSlice(s []int) {s[0] = 999 // Modificamos el elemento del array subyacente.}
func main() {miSlice := []int{10, 20, 30}modificarSlice(miSlice)fmt.Println(miSlice[0]) // Imprime 999. El original SI cambió!!!}La función append es la que le da su dinamismo. Si añades un elemento y aún hay capacidad en el array subyacente,
simplemente aumenta la longitud del slice. Pero si la capacidad se agota, append hace algo crucial:
-
Crea un nuevo array más grande (generalmente duplicando la capacidad anterior).
-
Copia todos los elementos del array viejo al nuevo.
-
Crea un nuevo slice header que apunta al nuevo array.
-
Devuelve este nuevo slice.
Por eso siempre debes asignar el resultado de append: miSlice = append(miSlice, nuevoElemento). Si no lo haces,
estarías trabajando con la versión antigua del slice que apunta al array original, lo cual puede llevar a errores
sutiles y difíciles de depurar si la capacidad original se agotó!!! Tranqui que es cuestion de practica!!!
Operaciones Esenciales con Slices: Manipulando Vistas
Muchachones ahora que entendemos cómo funcionan los slices por debajo, es crucial saber cómo operar con ellos!!!.
Cortar Slices: Creando Sub-vistas, puedes crear un nuevo slice a partir de uno existente usando la sintaxis de ” corte” (slicing). Esta es la forma principal de obtener sub-vistas del array subyacente. La sintaxis es slice[low:high ].
-
low: Es el índice de inicio (inclusivo). El elemento en este índice es el primer elemento del nuevo slice. Si se omite, por defecto es 0.
-
high: Es el índice de fin (exclusivo). El elemento en este índice NO se incluye en el nuevo slice. Si se omite, por defecto es la longitud del slice original.
Veamos algunos ejemplos mis bros:
package main
import "fmt"
func main() { numeros := []int{10, 20, 30, 40, 50, 60, 70, 80}
fmt.Println("Original:", numeros) // Original: [10 20 30 40 50 60 70 80]
// Slice desde el índice 2 (inclusive) hasta el 5 (exclusivo) // Esto es: 30, 40, 50 subSlice1 := numeros[2:5] fmt.Println("subSlice1 (numeros[2:5]):", subSlice1) // subSlice1 (numeros[2:5]): [30 40 50]
// Slice desde el inicio (omitimos low) hasta el índice 4 (exclusivo) // Esto es: 10, 20, 30, 40 subSlice2 := numeros[:4] fmt.Println("subSlice2 (numeros[:4]):", subSlice2) // subSlice2 (numeros[:4]): [10 20 30 40]
// Slice desde el índice 3 (inclusive) hasta el final (omitimos high) // Esto es: 40, 50, 60, 70, 80 subSlice3 := numeros[3:] fmt.Println("subSlice3 (numeros[3:]):", subSlice3) // subSlice3 (numeros[3:]): [40 50 60 70 80]
// Copia completa del slice (desde inicio hasta fin) // Aunque es una copia "superficial" del slice header, apunta al mismo array subyacente. copiaSlice := numeros[:] fmt.Println("copiaSlice (numeros[:]):", copiaSlice) // copiaSlice (numeros[:]): [10 20 30 40 50 60 70 80]
// Un caso especial: sub-slice con tres índices: [low:high:max] // max es la capacidad del nuevo slice, limitando hasta dónde puede crecer. // El 'max' debe ser mayor o igual que 'high'. // `newSlice := oldSlice[low : high : max]` // `len` del nuevo slice será `high - low` // `cap` del nuevo slice será `max - low` // Es útil para controlar la capacidad del nuevo slice, evitando compartir el resto del array subyacente. sliceConCapacidadLimitada := numeros[2:5:5] // len = 3, cap = 3 (5-2) fmt.Println("sliceConCapacidadLimitada (numeros[2:5:5]):", sliceConCapacidadLimitada, "len:", len(sliceConCapacidadLimitada), "cap:", cap(sliceConCapacidadLimitada)) // Si intentamos añadir un elemento a sliceConCapacidadLimitada que exceda su cap (3), // se creará un nuevo array subyacente, ¡incluso si el array original tenía más espacio!}Es importante que recuerden que, a menos que uses copy() (lo veremos a continuación) o la capacidad de un slice se agote, con append(), todas estas operaciones de corte comparten el mismo array subyacente. Esto significa que modificar un elemento en un sub-slice puede afectar al slice original y a otros sub-slices que apunten a la misma región de memoria.
Copiar Slices: Creando Duplicados Independientes, a veces, no quieres una vista, sino una copia independiente de un slice. Para eso, usamos la función copy(destino, origen). Esta función copia los elementos desde el slice origen al slice destino. Copia la menor cantidad de elementos entre len(destino) y len(origen).
package main
import "fmt"
func main() { original := []int{1, 2, 3, 4, 5} copia := make([]int, len(original)) // Creamos un nuevo slice con la misma longitud
n := copy(copia, original) // n es el número de elementos copiados fmt.Println("Original:", original) // Original: [1 2 3 4 5] fmt.Println("Copia:", copia) // Copia: [1 2 3 4 5] fmt.Println("Elementos copiados:", n) // Elementos copiados: 5
// Modificamos la copia copia[0] = 99 fmt.Println("Original después de modificar copia:", original) // Original después de modificar copia: [1 2 3 4 5] (sin cambios) fmt.Println("Copia después de modificar copia:", copia) // Copia después de modificar copia: [99 2 3 4 5]
// Si el destino es más pequeño, solo se copian los que caben parteCopia := make([]int, 3) n = copy(parteCopia, original) // Copia 1, 2, 3 fmt.Println("Parte de la copia:", parteCopia) // Parte de la copia: [1 2 3] fmt.Println("Elementos copiados (parte):", n) // Elementos copiados (parte): 3}La función copy es la forma segura de garantizar que estás trabajando con una nueva porción de memoria, y que los cambios que hagas en el slice copiado no afectarán al slice original.
Eliminar Elementos de Slices: Rebanando y Concatenando. Go no tiene una función delete() para slices directamente como la tiene para mapas. Para “eliminar” un elemento, debes crear un nuevo slice que contenga todos los elementos excepto el que quieres quitar. Esto generalmente se logra concatenando dos partes del slice original.
package main
import "fmt"
func main() { frutas := []string{"Manzana", "Banana", "Cereza", "Dátil", "Uva"} fmt.Println("Original:", frutas) // Original: [Manzana Banana Cereza Dátil Uva]
// Eliminar el elemento en el índice 2 ("Cereza") // Concatenamos la parte antes del índice 2 con la parte después del índice 2. // `frutas[:2]` -> ["Manzana", "Banana"] // `frutas[3:]` -> ["Dátil", "Uva"] frutas = append(frutas[:2], frutas[3:]...) // Los `...` "desempaquetan" el segundo slice fmt.Println("Después de eliminar Cereza:", frutas) // Después de eliminar Cereza: [Manzana Banana Dátil Uva]
// Eliminar el primer elemento ("Manzana") frutas = frutas[1:] // Simplemente creamos un nuevo slice que empieza un índice más allá fmt.Println("Después de eliminar Manzana:", frutas) // Después de eliminar Manzana: [Banana Dátil Uva]
// Eliminar el último elemento ("Uva") frutas = frutas[:len(frutas)-1] // Creamos un nuevo slice que termina un índice antes fmt.Println("Después de eliminar Uva:", frutas) // Después de eliminar Uva: [Banana Dátil]
// Caso general para eliminar en un índice 'i': // slice = append(slice[:i], slice[i+1:]...)}Es importante entender que esta operación de “eliminación” no reduce la capacidad del array subyacente. Simplemente crea una nueva vista (o un nuevo array si la capacidad se agota en el append) sin el elemento deseado. ojo muchachones, si trabajas con slices muy grandes y realizas muchas eliminaciones, la memoria del array subyacente anterior podría no ser liberada hasta que el recolector de basura determine que ya no hay referencias a ella.
Ejercicios de Comprensión
Ejercicio 1: El Fantasma en la Máquina (Slices y Memoria Compartida)
Objetivo: Demostrar que entiendes que los slices son vistas sobre un mismo array.
package main
import "fmt"
func main() { // 1. Crea un slice llamado universo con los valores 1, 2, 3, 4, 5, 6, 7, 8, 9, 10. universo := []int{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}
// 2. Crea dos nuevos slices a partir de universo: // sistemaSolar que contenga los elementos del índice 2 al 6 (3, 4, 5, 6, 7). sistemaSolar := universo[2:7] // Recordatorio: el segundo índice es exclusivo
// planetasRocosos que sea un slice de sistemaSolar y contenga los elementos del índice 0 al 3 (3, 4, 5, 6). planetasRocosos := sistemaSolar[0:4] // Recordatorio: el segundo índice es exclusivo
// 3. Imprime los tres slices para ver su estado inicial. fmt.Println("--- Estado Inicial ---") fmt.Println("universo:", universo) fmt.Println("sistemaSolar:", sistemaSolar) fmt.Println("planetasRocosos:", planetasRocosos)
// 4. Ahora, modifica el primer elemento de planetasRocosos y asígnale el valor 99. planetasRocosos[0] = 99
// 5. Vuelve a imprimir los tres slices: universo, sistemaSolar y planetasRocosos. fmt.Println("\n--- Estado Después de la Modificación ---") fmt.Println("universo:", universo) fmt.Println("sistemaSolar:", sistemaSolar) fmt.Println("planetasRocosos:", planetasRocosos)}Pregunta para tu cerebro muchachon: Por qué al cambiar planetasRocosos se modificaron también los otros dos
slices??? Explícalo en términos de array subyacente, puntero, longitud y capacidad.
Respuesta Detallada:
Al modificar planetasRocosos[0], los otros dos slices (universo y sistemaSolar) también se vieron afectados porque
todos los slices comparten el mismo array subyacente.
-
universo: Este slice fue el primero en ser creado y, por lo tanto, tiene un puntero que apunta al inicio del array subyacente completo[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]. Su longitud es 10 y su capacidad es 10. -
sistemaSolar: Se creó a partir deuniversousandouniverso[2:7]. Esto significa que su puntero también apunta al mismo array subyacente queuniverso, pero específicamente al elemento en el índice 2 de ese array subyacente (que es el valor3). Su longitud es7 - 2 = 5(elementos3, 4, 5, 6, 7). Su capacidad es la capacidad del array subyacente desde la posición de su puntero hasta el final:10 - 2 = 8. -
planetasRocosos: Se creó a partir desistemaSolarusandosistemaSolar[0:4]. Su puntero apunta al mismo lugar que el puntero desistemaSolar(es decir, al valor3dentro del array subyacente original). Su longitud es4 - 0 = 4(elementos3, 4, 5, 6). Su capacidad es la capacidad desistemaSolardesde la posición de su puntero hasta el final:8 - 0 = 8.
Cuando hacemos planetasRocosos[0] = 99, estamos modificando el elemento al que apunta el puntero de planetasRocosos
en el array subyacente. Dado que sistemaSolar y universo también tienen punteros que apuntan (directa o
indirectamente) a ese mismo array subyacente, y específicamente a esa misma posición de memoria, el cambio se refleja en
ellos.
-
planetasRocosos[0]modificó el valor3del array subyacente. -
Como
sistemaSolartambién es una vista de ese mismo array subyacente, su primer elemento (que antes era3) ahora es99. -
Y como
universoes la vista completa de ese array subyacente, su tercer elemento (índice 2, que antes era3) también se convierte en99.
Es la memoria compartida a través del puntero del slice lo que explica este comportamiento.
Ejercicio 2: El Coste del Crecimiento (Append y Capacidad)
Objetivo: Visualizar cómo Go gestiona el crecimiento de un slice y entender sus implicaciones de rendimiento.
package main
import "fmt"
func main() { // 1. Crea un slice vacío de enteros con una capacidad inicial de 3, usando la función make: numeros := make([]int, 0, 3)
fmt.Println("--- Análisis de Crecimiento de Slice ---") // 2. Escribe un bucle for que itere 15 veces. for i := 0; i < 15; i++ { // 3. Dentro del bucle: // Añade el número de la iteración al slice usando append. Recuerda reasignar el slice: numeros = append(numeros, i)
// Imprime la longitud (len), la capacidad (cap) del slice // y la dirección de memoria de su primer elemento (&numeros[0]) en cada iteración. // Ojo: solo puedes imprimir la dirección si el slice no está vacío. if len(numeros) > 0 { fmt.Printf("Iteración %2d: len = %2d, cap = %2d, addr = %p\n", i, len(numeros), cap(numeros), &numeros[0]) } else { fmt.Printf("Iteración %2d: len = %2d, cap = %2d\n", i, len(numeros), cap(numeros)) } }}Salida Esperada (puede variar ligeramente en direcciones de memoria):
--- Análisis de Crecimiento de Slice ---Iteración 0: len = 1, cap = 3, addr = 0xc000018060Iteración 1: len = 2, cap = 3, addr = 0xc000018060Iteración 2: len = 3, cap = 3, addr = 0xc000018060Iteración 3: len = 4, cap = 6, addr = 0xc00001a000 <- Capacidad cambióIteración 4: len = 5, cap = 6, addr = 0xc00001a000Iteración 5: len = 6, cap = 6, addr = 0xc00001a000Iteración 6: len = 7, cap = 12, addr = 0xc00001c000 <- Capacidad cambióIteración 7: len = 8, cap = 12, addr = 0xc00001c000Iteración 8: len = 9, cap = 12, addr = 0xc00001c000Iteración 9: len = 10, cap = 12, addr = 0xc00001c000Iteración 10: len = 11, cap = 12, addr = 0xc00001c000Iteración 11: len = 12, cap = 12, addr = 0xc00001c000Iteración 12: len = 13, cap = 24, addr = 0xc000020000 <- Capacidad cambióIteración 13: len = 14, cap = 24, addr = 0xc000020000Iteración 14: len = 15, cap = 24, addr = 0xc000020000Preguntas para tu cerebro muchachon:
- Observa la salida. En qué iteraciones exactas cambia la capacidad del slice? La capacidad cambia en las iteraciones donde la longitud del slice excede su capacidad actual:
-
De 3 a 6 en la iteración 3 (cuando la longitud pasa de 3 a 4).
-
De 6 a 12 en la iteración 6 (cuando la longitud pasa de 6 a 7).
-
De 12 a 24 en la iteración 12 (cuando la longitud pasa de 12 a 13).
-
Qué patrón sigue el crecimiento de la capacidad? (Suele duplicarse, pero no siempre).
En este ejemplo, la capacidad se duplica cada vez que se agota. El patrón general de crecimiento de capacidad en Go es que, si la capacidad actual es menor a 1024, Go tiende a duplicarla. Si es 1024 o más, crece en un factor de 1.25 (o 25% más). Sin embargo, la implementación exacta puede variar ligeramente entre versiones de Go y arquitecturas, pero la estrategia principal es minimizar las reasignaciones costosas.
-
Qué ocurre con la dirección de memoria del primer elemento cada vez que la capacidad cambia? Qué te dice esto sobre lo que
appendestá haciendo por debajo?Cada vez que la capacidad cambia, la dirección de memoria del primer elemento (
addr) también cambia. Esto nos dice que cuando la capacidad de un slice se agota y se llama aappendpara añadir un nuevo elemento, Go:
-
Asigna un nuevo bloque de memoria en una ubicación diferente, que es más grande que el anterior.
-
Copia todos los elementos del array subyacente antiguo a este nuevo y más grande array.
-
Actualiza el puntero del slice para que apunte al inicio de este nuevo array.
Este proceso de reasignación y copia es costoso en términos de rendimiento, especialmente con slices muy grandes. Por eso, entender el crecimiento de la capacidad es fundamental mis bros y, en escenarios donde se conoce un tamaño aproximado, es buena práctica inicializar slices con una capacidad adecuada usando
make([]T, len, cap)para evitar reasignaciones innecesarias.
Ejercicio 3: El Guardián del Error
Objetivo: Aplicar el manejo de errores idiomático de Go.
package main
import ( "errors" // Importamos el paquete errors para crear errores simples "fmt")
// 1. Crea una función dividir(dividendo float64, divisor float64) que devuelva dos valores:// un float64 (el resultado) y un error.func dividir(dividendo float64, divisor float64) (float64, error) { // 2. Dentro de dividir, si el divisor es 0, la función debe devolver 0 y un nuevo error. // Para crear un error simple, usa el paquete errors: // return 0, errors.New("operación inválida: no se puede dividir por cero"). if divisor == 0 { return 0, errors.New("operación inválida: no se puede dividir por cero") }
// 3. Si el divisor no es cero, la función debe devolver el resultado de la división y nil como valor de error. return dividendo / divisor, nil}
func main() { fmt.Println("--- Manejo de Errores con la función dividir ---")
// En tu función main, llama a dividir dos veces: // Una vez con valores válidos (ej: 10.0, 2.0). resultado1, err1 := dividir(10.0, 2.0) // En ambos casos, usa el patrón if err != nil para comprobar si hubo un error. // Imprime un mensaje de éxito con el resultado si no hay error, o un mensaje de fallo con el texto del error si lo hay. // No dejes que el programa entre en pánico. if err1 != nil { fmt.Println("Error en la división 1:", err1) } else { fmt.Printf("Resultado de la división 1: %.2f\n", resultado1) }
// Una vez intentando dividir por cero. resultado2, err2 := dividir(5.0, 0.0) if err2 != nil { fmt.Println("Error en la división 2:", err2) } else { fmt.Printf("Resultado de la división 2: %.2f\n", resultado2) }
// Un ejemplo adicional para ilustrar resultado3, err3 := dividir(100.0, 4.0) if err3 != nil { fmt.Println("Error en la división 3:", err3) } else { fmt.Printf("Resultado de la división 3: %.2f\n", resultado3) }}Salida Esperada:
--- Manejo de Errores con la función dividir ---Resultado de la división 1: 5.00Error en la división 2: operación inválida: no se puede dividir por ceroResultado de la división 3: 25.00En este ejercicio se demuestra la forma idiomática de Go para manejar errores muchachones: las funciones devuelven un
error como uno de sus valores de retorno, y el llamador es obligado a comprobar explícitamente si ese error es
nil (indicando éxito) o no (indicando que se jodio). Esto hace que el manejo de errores sea explícito y que sea muy
difícil “olvidarse” de ellos, lo que lleva a un software más robusto.!!!
Nos vemos en el siguiente Capitulo, Activitos muchachones!!!