1Explica cómo nil funciona de forma distinta para punteros, slices, maps, channels, funciones e interfaces en Go.
En Go, nil es el valor cero de punteros, slices, maps, channels, funciones e interfaces, pero las operaciones sobre esos valores nil difieren según el tipo. Un puntero nil se puede comparar con nil, pero desreferenciarlo provoca panic. Un slice nil tiene longitud y capacidad 0 y se puede recorrer con range y usar con append. Un map nil se puede leer y recorrer con range, pero asignar en él provoca panic. Enviar a o recibir de un channel nil bloquea para siempre, y cerrar un channel nil provoca panic. Llamar a una función nil provoca panic. Una interface es nil solo cuando no tiene tipo dinámico ni valor dinámico; una interface que contiene un valor nil tipado, como un puntero nil, no es ella misma nil.
2¿Qué son los tipos comparables en Go y cómo afectan las reglas de comparabilidad a las claves de map, la igualdad y las restricciones de generics?
Los tipos comparables en Go son tipos cuyos valores se pueden comparar con `==` y `!=`. Los tipos básicos, punteros, channels, interfaces y structs/arrays cuyos campos o elementos son comparables son comparables; slices, maps y funciones no son comparables excepto con `nil`. Las claves de map deben ser comparables. La igualdad sigue las reglas de comparación del tipo, y la comparación de interfaces depende de los valores concretos dinámicos; si una interface comparada contiene un valor dinámico incomparable, la comparación provoca panic. En generics, la restricción predeclarada `comparable` permite que los parámetros de tipo se comparen con `==`/`!=` y se usen como claves de map.
3¿Cómo representa Go los bytes, las runes y el texto codificado en UTF-8, y por qué len(s) puede diferir del número de caracteres visibles para el usuario?
En Go, `byte` es un alias de `uint8` y representa un byte crudo, mientras que `rune` es un alias de `int32` y representa un code point Unicode. Un `string` es una secuencia de solo lectura de bytes, habitualmente texto codificado en UTF-8 pero capaz de contener bytes arbitrarios. `len(s)` devuelve el número de bytes, no runes ni caracteres visibles para el usuario. Indexar un string devuelve un byte; recorrer un string con range decodifica UTF-8 y produce índices de byte más runes. `len(s)` puede diferir del número de caracteres visibles porque UTF-8 puede usar varios bytes por code point y porque un carácter visible para el usuario puede estar compuesto por varios code points, como marcas combinantes o secuencias de emoji.
4¿En qué se diferencian los type aliases de los defined types y cuándo usarías cada uno?
Un defined type, como `type UserID int64`, crea un tipo nuevo y distinto con `int64` como underlying type. No es libremente asignable a `int64` sin conversión y puede tener sus propios métodos. Un type alias, como `type UserID = int64`, es solo otro nombre para el mismo tipo, de modo que se preservan la identidad de tipo y la asignabilidad. Usa defined types para modelado de dominio, type safety y métodos; usa aliases principalmente para refactorización, migración o compatibilidad sin introducir un tipo nuevo.
5¿Cómo trata Go los valores especiales de punto flotante y qué trampas de igualdad importan en sistemas backend?
Los `float32` y `float64` de Go usan un comportamiento al estilo IEEE-754, incluidos valores especiales como infinito positivo/negativo y NaN. Para `float64`, los ayudantes incluyen `math.Inf`, `math.IsInf`, `math.NaN` y `math.IsNaN`. NaN no es igual a nada, ni siquiera a sí mismo, de modo que `x == x` es false cuando `x` es NaN. La igualdad exacta sobre floats calculados también es arriesgada porque el redondeo y la precisión pueden hacer que valores matemáticamente iguales difieran; usa tolerancias adecuadas al dominio o evita floats para valores de negocio exactos como el dinero. Los floats se permiten como claves de map, pero las claves NaN son problemáticas porque la búsqueda en el map depende de la igualdad y NaN no se compara como igual, ni siquiera a sí mismo.
6Describe el embedding de structs en Go y cómo se comportan los campos y métodos promovidos.
El embedding de structs en Go consiste en declarar un campo por su tipo sin un nombre de campo explícito, por ejemplo `type User struct { Person }`. El valor embebido sigue siendo un campo real, accesible como `u.Person`, pero sus campos y métodos exportados/accesibles pueden promoverse de modo que los llamadores puedan escribir selectores como `u.Name` o `u.Greet()` como abreviatura de pasar por el campo embebido. El embedding es composición, no herencia clásica: el tipo exterior no es automáticamente un subtipo del tipo embebido. Si los selectores promovidos entran en conflicto, Go no adivina; los nombres ambiguos deben calificarse o no son seleccionables a través del valor exterior.
7Describe cómo el reslicing y la asignación de slices pueden hacer que varios slices compartan el mismo array subyacente, y qué errores puede provocar esto.
Un valor slice es una cabecera que apunta a un array subyacente. Asignar un slice o pasarlo a una función copia solo esa cabecera, no los elementos. El reslicing crea otra cabecera que apunta a un rango del mismo array de respaldo. Por tanto, varios slices pueden hacer alias del mismo almacenamiento: cambiar un elemento a través de un slice puede verse a través de otro, y hacer append a un slice puede sobrescribir datos visibles para otro si aún tiene capacidad sobrante. Los errores incluyen mutaciones sorprendentes, resultados corruptos, retención de arrays de respaldo grandes a través de sub-slices pequeños, y condiciones de carrera cuando los alias se usan de forma concurrente. Para evitar la compartición no intencionada, haz una copia defensiva con copy o append([]T(nil), s...), o limita la capacidad con una expresión de slice completa antes del append.
8Explica el crecimiento de un slice durante append a nivel conceptual y las implicaciones de rendimiento de la reasignación repetida.
Cuando append añade elementos a un slice, escribe en el array de respaldo existente si el slice tiene suficiente capacidad. Si la capacidad es insuficiente, Go asigna un array de respaldo más grande, copia los elementos existentes, escribe los nuevos elementos y devuelve una cabecera de slice que apunta al nuevo almacenamiento. La política exacta de crecimiento depende de la implementación, pero conceptualmente la capacidad crece lo bastante para que el append repetido sea eficiente de forma amortizada. Las reasignaciones repetidas siguen costando CPU por la copia, crean asignaciones, aumentan la presión sobre el GC y pueden romper la compartición con alias antiguos del slice. Si conoces el tamaño esperado, preasigna con make([]T, 0, n) al construir con append o con make([]T, n) al rellenar por índice para reducir reasignaciones.
9¿Qué son los patrones de registro en tiempo de init en Go y qué riesgos conllevan los efectos secundarios de blank imports y los registries globales?
Un patrón de registro en tiempo de init es aquel en el que un paquete registra una implementación en un registry compartido desde una función `init`. Un blank import como `_ "example.com/driver"` se usa a menudo para importar un paquete solo por sus efectos secundarios, provocando que se ejecute su función `init` aunque no se referencien nombres exportados. Es habitual para puntos de extensión de drivers, codecs, plugins, métricas o serializadores. Los riesgos son dependencias ocultas y efectos secundarios de arranque, estado global mutable, registro duplicado o sensible al orden, mayor dificultad para aislar pruebas y un cableado de dependencias menos explícito. Debe usarse de forma deliberada, documentarse con claridad y a menudo mitigarse con registro explícito, registries idempotentes/seguros ante concurrencia, o registries inyectables/reiniciables para las pruebas.
10¿Cómo interactúan defer, panic y los valores de retorno nombrados al implementar cleanup que puede modificar los errores devueltos?
Las funciones diferidas se ejecutan después de que se han asignado los valores de retorno pero antes de que la función devuelva el control a su llamador. Por eso, un closure diferido puede leer o modificar valores de retorno nombrados como un `err` nombrado. Esto se usa habitualmente para añadir errores de cleanup de `Close`, `Commit` u operaciones similares al error que se devuelve, idealmente preservando el error principal en lugar de sobrescribirlo. Durante el unwinding de un panic, las funciones diferidas siguen ejecutándose; una función diferida puede hacer recover y establecer un valor de retorno nombrado, pero eso debe limitarse a límites intencionados de panic. Ten cuidado de no sombrear una variable de retorno nombrada como `err`, porque el defer podría entonces observar o modificar una variable distinta de la pretendida.
11¿Cómo funcionan errors.Is, errors.As y %w en las cadenas de error wrapping de Go?
`fmt.Errorf` con `%w` crea un nuevo error que envuelve un error subyacente al tiempo que añade contexto. Los wrappers exponen los errores subyacentes mediante `Unwrap`, formando una cadena o árbol que la biblioteca estándar puede inspeccionar. `errors.Is(err, target)` comprueba si `err` o algo que envuelve coincide con un error objetivo. `errors.As(err, &target)` comprueba si `err` o algo que envuelve es asignable al tipo objetivo y almacena el valor coincidente en el puntero proporcionado.
12¿Qué son los errores centinela (sentinel errors) y cuáles son las ventajas y desventajas frente a errores tipados personalizados o modelos de error de dominio más ricos?
Un error centinela (sentinel error) es un valor de error con nombre, a menudo una variable a nivel de paquete como `var ErrNotFound = errors.New("not found")`, usado para representar una condición específica que los llamadores pueden comprobar, normalmente con `errors.Is` cuando es posible el wrapping. Los centinelas son simples y útiles para categorías amplias y estables, pero los centinelas exportados pasan a formar parte de la API y pueden acoplar a los llamadores a valores concretos. Los errores tipados personalizados pueden llevar campos estructurados y detectarse con `errors.As`. Los modelos de error de dominio más ricos clasifican los fallos por kind/code y pueden incluir mensajes seguros o metadatos, lo cual es útil cuando los llamadores necesitan un comportamiento estable más allá de un único valor de error fijo.
13¿Cómo debería un backend en Go mapear errores internos a respuestas útiles para el cliente y, al mismo tiempo, dar a los operadores señales diagnosticables?
Un backend en Go debe traducir los errores internos en un límite de aplicación o de transporte a categorías y respuestas estables y seguras para el cliente. Esas categorías deben mapearse a códigos de estado HTTP apropiados o a estados de transporte equivalentes, con mensajes seguros y códigos legibles por máquina en lugar de errores internos en bruto. Los operadores deben seguir recibiendo información de diagnóstico mediante logs estructurados, trazas, métricas, IDs de correlación/request y causas subyacentes preservadas. El logging suele hacerse una sola vez en un límite que tenga contexto de la request, para evitar tanto fallos no registrados como logs duplicados ruidosos.
14¿Cómo se implementan correctamente tipos de error personalizados en Go y cómo afecta `errors.Join` a la inspección y al manejo de errores en rutas de limpieza?
Un tipo de error personalizado en Go satisface `error` implementando `Error() string`. También puede llevar campos estructurados e implementar `Unwrap() error` para exponer una causa subyacente. Importa el receptor puntero frente al de valor: un receptor puntero significa que solo `*T` satisface `error`, mientras que un receptor de valor suele hacer que tanto `T` como `*T` lo satisfagan, lo que afecta a la copia y al tipo que los llamadores deben usar con `errors.As`. `errors.Join` combina varios errores en uno; `errors.Is` y `errors.As` inspeccionan los hijos unidos. Esto es útil cuando deben devolverse tanto un error de la operación principal como un error de limpieza/diferido sin perder ninguno de los dos fallos.
15¿Cómo funciona select con channels, incluyendo la elección del case listo, los cases default y las operaciones cancelables?
select espera en varias operaciones de channel y ejecuta un case cuyo envío o recepción pueda continuar. Si ningún case de channel está listo, se bloquea a menos que haya un case default; default se ejecuta de inmediato solo cuando ninguna operación de channel puede continuar, lo cual es útil para intentos de envío/recepción no bloqueantes. Si varios cases están listos, Go elige uno de forma pseudoaleatoria y no por el orden del código fuente. Las operaciones de channel cancelables suelen añadir un case que recibe de ctx.Done() para que la goroutine deje de esperar cuando el context se cancela o expira.