1Explica cómo funcionan los valores cero (zero values) de Go en los tipos integrados y en los tipos similares a referencias, y por qué importan al declarar variables sin inicialización explícita.
En Go, una variable declarada sin un inicializador explícito se inicializa automáticamente al valor cero de su tipo. Los tipos numéricos pasan a ser 0, bool pasa a ser false, string pasa a ser "", y los arrays o structs se ponen a cero elemento a elemento o campo a campo. Los tipos similares a punteros o a referencias, como punteros, slices, maps, channels, funciones e interfaces, tienen nil como valor cero. Esto importa porque las variables de Go y los campos de struct omitidos arrancan en un estado determinista en lugar de contener basura, y muchas APIs están diseñadas para que el valor cero sea un valor por defecto útil, aunque algunos valores nil siguen necesitando inicialización antes de ciertas operaciones.
2¿Cómo maneja Go la igualdad de structs y qué ocurre cuando un struct contiene campos incomparables?
Los valores struct en Go se pueden comparar con `==` y `!=` solo cuando cada campo del struct es comparable. La igualdad compara los campos correspondientes usando la propia regla de igualdad de cada campo. Si un struct contiene un campo incomparable como un slice, un map o una función, el tipo struct no es comparable, y comparar dos valores de ese tipo struct con `==` es un error en tiempo de compilación. Para tales structs, usa lógica de comparación personalizada o un ayudante adecuado de igualdad profunda, especialmente en pruebas.
3Explica la inmutabilidad de string en Go y la relación entre string, []byte, bytes.Buffer y strings.Builder.
Un `string` en Go es una secuencia inmutable de bytes, a menudo texto UTF-8 pero no necesariamente UTF-8 válido. No se puede modificar un string in situ; para cambiar el contenido normalmente se convierte a `[]byte` para ediciones a nivel de byte o a `[]rune` para ediciones a nivel de code point, y luego se vuelve a convertir. Las conversiones normales entre `string` y `[]byte` copian datos y pueden asignar memoria, así que las conversiones repetidas o la concatenación repetida en bucles pueden ser costosas. `strings.Builder` está optimizado para construir strings de forma eficiente, mientras que `bytes.Buffer` es un buffer de bytes mutable útil para datos orientados a bytes e I/O y también puede producir un string.
4Explica 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.
5¿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.
6¿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.
7Describe la diferencia entre arrays y slices en Go, incluyendo cómo se comportan la longitud, la capacidad y el almacenamiento subyacente.
Un array en Go tiene una longitud fija que forma parte de su tipo, como [3]int; almacena sus elementos directamente, y asignar o pasar un array copia todo el valor del array. Un slice, como []int, es un pequeño descriptor sobre un array subyacente: conceptualmente contiene un puntero a los elementos, una longitud y una capacidad. La longitud de un slice es el número de elementos visibles; su capacidad es cuántos elementos se pueden usar desde el inicio del slice antes de llegar al final del array de respaldo. Los slices son flexibles: rebanar (reslicing) cambia el descriptor, y append puede reutilizar el mismo array subyacente si la capacidad lo permite o asignar uno nuevo si no.
8¿Cómo se comporta el tipo map de Go respecto a los tipos de clave, las claves ausentes, los maps nil y el orden de iteración?
Los tipos de clave de un map en Go deben ser comparables; slices, maps y funciones no se pueden usar directamente como claves. Buscar una clave que no está presente devuelve el valor cero del tipo del elemento, por lo que se usa la forma comma-ok (`v, ok := m[k]`) para distinguir la ausencia de un valor cero presente. Un map nil se puede leer y recorrer con range, pero asignar en él provoca panic; hay que inicializarlo antes de escribir. El orden de iteración de un map no está especificado y el código no debe depender de él.
9Describe 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.
10Explica 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.
11¿Cómo funciona el blank identifier en Go para valores no usados, imports y comprobaciones de interfaces en tiempo de compilación?
El blank identifier `_` es un marcador de solo escritura. Asignarle descarta el valor y no crea una variable usable. Se usa para ignorar valores de retorno o variables de bucle no necesarios, para importar un paquete solo por sus efectos secundarios con `import _ "pkg"`, y para hacer comprobaciones de implementación de interfaces en tiempo de compilación como `var _ io.Reader = (*MyReader)(nil)`. Un blank import sigue ejecutando la inicialización del paquete importado. Una asignación de comprobación de interfaz falla al compilar si el method set del tipo concreto no satisface la interfaz.
12¿Cómo funciona el orden de inicialización de paquetes en Go, incluidas las funciones init y las dependencias importadas?
Go inicializa los paquetes en orden de dependencias. Las dependencias importadas de un paquete se inicializan antes que el paquete importador. Dentro de un paquete, las variables a nivel de paquete se inicializan antes que cualquier función `init`, con la inicialización de variables ordenada por dependencia y orden de declaración según define el lenguaje. Después se ejecutan automáticamente las funciones `init` del paquete; un paquete puede tener varias funciones `init`, y no pueden llamarse directamente. Cada paquete se inicializa una sola vez. En un ejecutable, primero se inicializa el grafo de imports, luego se inicializa el paquete `main` y finalmente se llama a `main.main`.
13Explica las reglas de visibilidad de paquetes en Go, incluidos los identificadores exportados y la convención del directorio internal/.
En Go, la visibilidad de paquetes se controla por el nombre de los identificadores, no por palabras clave de acceso. Un identificador cuyo nombre empieza por una letra Unicode mayúscula está exportado y puede referenciarse desde otros paquetes; los demás identificadores no están exportados y solo son usables desde el mismo paquete. Esto se aplica a funciones, tipos, métodos, variables, constantes y campos de struct. Los paquetes usan identificadores exportados para definir su API pública y mantienen los detalles de implementación sin exportar. Por separado, un paquete situado bajo un directorio `internal/` solo puede ser importado por código cuya ruta de import esté dentro del árbol padre de ese directorio `internal`; esto lo impone la toolchain de Go.
14¿Cómo funciona defer en Go, incluido el orden de ejecución, el momento de evaluación de argumentos y la interacción con los valores de retorno?
`defer` programa una llamada a función para que se ejecute cuando la función circundante está saliendo, ya sea por un return normal o por el unwinding de un panic. Varias llamadas diferidas se ejecutan en orden last-in, first-out. El valor de la función diferida y sus argumentos se evalúan de inmediato cuando se ejecuta la sentencia `defer`, pero la llamada en sí se ejecuta después. Con valores de retorno con nombre, una sentencia return asigna primero los valores de retorno y luego se ejecutan las funciones diferidas, de modo que un closure diferido puede observar o modificar las variables de resultado con nombre antes de que el llamador las reciba. Esto hace que `defer` sea útil para la limpieza, como cerrar archivos, desbloquear mutexes y liberar recursos.
15Explica el modelo de manejo de errores de Go y las formas convencionales de crear, devolver y comprobar errores.
Go trata los errores como valores ordinarios, no como excepciones. La interfaz incorporada `error` la satisface cualquier tipo con un método `Error() string`. Las funciones convencionalmente devuelven un `error` como último resultado, donde `nil` significa éxito y un error no nil significa que el llamador debe manejar o propagar el fallo. Los errores simples se crean habitualmente con `errors.New`, los errores formateados con `fmt.Errorf`, y los llamadores suelen comprobar `if err != nil { ... }`.
16How should panic recovery be handled in Go backend services, including what happens when a goroutine panics and when a process should recover versus crash?
A panic unwinds the current goroutine, running its deferred functions. `recover` only works when called from a deferred function in that same goroutine; one goroutine cannot recover another goroutine's panic. If a panic is not recovered, the process crashes. In backend services, recovery should usually be placed at isolation boundaries such as request handlers, RPC middleware, or worker goroutine entrypoints so one failing request or job does not take down the whole service. But if a panic may have corrupted shared state or made process integrity untrustworthy, it is safer to let the process crash and restart rather than recover and continue blindly.
17¿Qué son los channels nil en Go y cómo pueden romper código por accidente o deshabilitar cases de select de forma intencionada?
Un channel nil es una variable channel cuyo valor es nil, a menudo porque no se inicializó con make o se asignó explícitamente a nil. Enviar a o recibir de un channel nil se bloquea para siempre. En un select, un case que involucra un channel nil nunca está listo, de modo que asignar una variable channel a nil puede deshabilitar ese case de forma intencionada. Usar accidentalmente un channel nil puede hacer que las goroutines se cuelguen o que la lógica de select deje de manejar eventos esperados.
18¿En qué se diferencian las operaciones atómicas de sync/atomic de la sincronización basada en mutex y cuándo son apropiadas?
sync/atomic proporciona operaciones indivisibles sobre ubicaciones de memoria individuales, como load, store, add, swap y compare-and-swap, con garantías de sincronización/orden de memoria. Un mutex protege una sección crítica, de modo que puede custodiar código arbitrario e invariantes que involucran varias lecturas, escrituras o campos. Los atomics son apropiados para estado simple e independiente como contadores, flags, números de secuencia o estructuras lock-free diseñadas con cuidado. Prefiere un mutex cuando las operaciones son compuestas, varios valores deben permanecer consistentes o la versión atómica sería difícil de razonar o de demostrar correcta.
19¿Cómo deben diseñarse la propiedad de los channels y el ciclo de vida de las goroutines para evitar fugas de goroutines?
Diseña las goroutines con un propietario explícito, una señal clara de apagado y una ruta de salida garantizada. El lado productor suele ser el dueño de cerrar un channel, especialmente un channel de salida; los receptores no deben cerrar un channel mientras los emisores aún puedan estar activos. Todo envío, recepción, bucle, timer o llamada externa bloqueante debe completarse de forma garantizada o poder desbloquearse ante cancelación, habitualmente mediante context.Context o un channel done. Usa WaitGroup, errgroup o una coordinación similar para esperar a los workers y cerrar channels solo después de que los emisores salgan.
20What are common causes of goroutine leaks in Go services, and how do you detect and fix them in production?
Common goroutine leaks in Go services come from goroutines blocked forever on channel sends or receives, waiting on other blocking operations without cancellation, stuck I/O without deadlines, background loops or tickers that never stop, and request-scoped goroutines that outlive the request. In production, you look for sustained growth in goroutine count and related symptoms, then inspect goroutine dumps or pprof goroutine profiles to see where goroutines are stuck. Fixing the leak means changing the code so those goroutines can exit: add cancellation and deadlines, stop tickers, close channels correctly, avoid detached request goroutines, and bound concurrency where needed.