Preparación de entrevista Go

Preguntas de entrevista para desarrolladores backend Go

Selección de preguntas Go para desarrolladores backend, agrupadas por temas y tomadas del mismo catálogo que usa la práctica de EngineerSpeak.

Empezar una entrevista IA de GoNo se requiere tarjeta de crédito. 1 sesión gratuita disponible.
Práctica de entrevistas técnicas en inglésDiseñado para hablantes no nativos que desean practicar entrevistas técnicas en inglés.

Sistema de tipos

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.

package main

import "fmt"

type Config struct {
	Port    int
	Debug   bool
	Name    string
	Tags    []string
	Options map[string]string
}

func main() {
	var c Config
	fmt.Printf("port=%d debug=%v name=%q tags==nil:%v options==nil:%v\n",
		c.Port, c.Debug, c.Name, c.Tags == nil, c.Options == nil)
}
Probar responder esta pregunta con un coach de IA

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.

package main

import "fmt"

type UserID struct {
    Name string
    Age  int
}

type UserProfile struct {
    Name string
    Tags []string
}

func main() {
    a := UserID{"Ann", 30}
    b := UserID{"Ann", 30}
    fmt.Println(a == b)

    x := UserProfile{Name: "Ann", Tags: []string{"go"}}
    y := UserProfile{Name: "Ann", Tags: []string{"go"}}
    _, _ = x, y
    // fmt.Println(x == y) // compile error: struct containing []string cannot be compared
}
Probar responder esta pregunta con un coach de IA

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.

package main

import (
    "bytes"
    "fmt"
    "strings"
)

func main() {
    var sb strings.Builder
    sb.WriteString("hello")
    sb.WriteByte(' ')
    sb.WriteString("world")
    fmt.Println(sb.String())

    var buf bytes.Buffer
    buf.Write([]byte{0x48, 0x69})
    buf.WriteByte('!')
    fmt.Println(buf.String())

    s := "cat"
    b := []byte(s) // normally copies
    b[0] = 'b'
    fmt.Println(s, string(b))
}
Probar responder esta pregunta con un coach de IA

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.

package main

import "fmt"

type MyErr struct{}

func (*MyErr) Error() string { return "my error" }

func returnsTypedNil() error {
	var e *MyErr = nil
	return e
}

func main() {
	var p *int = nil
	var s []int = nil
	var m map[string]int = nil
	var err error = returnsTypedNil()

	fmt.Println(p == nil)
	fmt.Println(len(s), cap(s), s == nil)
	fmt.Println(m["missing"])
	fmt.Println(err == nil)
}
Probar responder esta pregunta con un coach de IA

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.

package main

import "fmt"

type Point struct{ X, Y int }      // comparable
type Bag struct{ Items []string }  // not comparable because of slice field

func Contains[T comparable](xs []T, target T) bool {
    for _, x := range xs {
        if x == target {
            return true
        }
    }
    return false
}

func main() {
    p1, p2 := Point{1, 2}, Point{1, 2}
    fmt.Println(p1 == p2)

    m := map[Point]string{p1: "value"}
    fmt.Println(m[p2])

    fmt.Println(Contains([]string{"a", "b"}, "b"))

    var a any = []int{1}
    var b any = []int{1}
    _, _ = a, b
    // fmt.Println(a == b) // panic: comparing uncomparable type []int
}
Probar responder esta pregunta con un coach de IA

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.

package main

import "fmt"

func main() {
    s := "é🙂"

    fmt.Println(len(s))         // bytes: é is 2 bytes, 🙂 is 4 bytes
    fmt.Println(len([]rune(s))) // Unicode code points

    fmt.Printf("first byte: %x\n", s[0])

    for i, r := range s {
        fmt.Printf("byte index %d: %q U+%04X\n", i, r, r)
    }
}
Probar responder esta pregunta con un coach de IA

Estructuras de datos

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.

package main

import "fmt"

func main() {
	a := [3]int{1, 2, 3}
	b := a
	b[0] = 99
	fmt.Println(a, b)

	s := []int{1, 2, 3}
	t := s
	t[0] = 99
	fmt.Println(s, t)
	fmt.Println(len(s), cap(s))
}
Probar responder esta pregunta con un coach de IA

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.

package main

import "fmt"

func main() {
    counts := map[string]int{"a": 0, "b": 2}

    fmt.Println(counts["missing"]) // zero value for int

    v, ok := counts["a"]
    fmt.Println(v, ok) // present even though value is zero

    var m map[string]int
    fmt.Println(m["x"]) // read from nil map is OK
    // m["x"] = 1     // panic: assignment to entry in nil map

    m = make(map[string]int)
    m["x"] = 1

    for k, v := range counts {
        fmt.Println(k, v) // order is not guaranteed
    }
}
Probar responder esta pregunta con un coach de IA

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.

package main

import "fmt"

func main() {
	base := []int{1, 2, 3, 4}
	a := base[:2] // len 2, cap 4
	b := base[2:] // len 2, cap 2

	a[0] = 99
	fmt.Println(base, a, b)

	a = append(a, 77) // reuses base's backing array, overwrites base[2]
	fmt.Println(base, a, b)

	c := append([]int(nil), base[:2]...) // defensive copy
	c[0] = 42
	fmt.Println(base, c)
}
Probar responder esta pregunta con un coach de IA

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.

package main

func collectNoPrealloc(input []int) []int {
	var out []int
	for _, v := range input {
		out = append(out, v*2)
	}
	return out
}

func collectPrealloc(input []int) []int {
	out := make([]int, 0, len(input))
	for _, v := range input {
		out = append(out, v*2)
	}
	return out
}
Probar responder esta pregunta con un coach de IA

Semántica del lenguaje

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.

package main

import "fmt"

func lookup() (string, bool) {
    return "gopher", true
}

func main() {
    name, _ := lookup() // ignore the bool
    fmt.Println(name)

    for i, _ := range []int{10, 20} {
        fmt.Println(i)
    }
}
Probar responder esta pregunta con un coach de IA

Paquetes

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`.

// a/a.go
package a

import "fmt"

var V = func() int {
    fmt.Println("a var")
    return 1
}()

func init() { fmt.Println("a init") }

// main.go
package main

import (
    "fmt"
    "example/a"
)

var M = func() int {
    fmt.Println("main var", a.V)
    return 2
}()

func init() { fmt.Println("main init") }

func main() { fmt.Println("main") }
Probar responder esta pregunta con un coach de IA

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.

// module example.com/app

// internal/store/store.go
package store

type Client struct { // exported type
    dsn string // unexported field
}

func New(dsn string) *Client { return &Client{dsn: dsn} } // exported
func parseDSN(s string) string { return s }                // unexported

// cmd/api/main.go -- allowed: inside example.com/app tree
package main

import "example.com/app/internal/store"

func main() {
    c := store.New("db")
    _ = c
    // c.dsn is not accessible here: field is unexported.
}

// Code outside example.com/app cannot import example.com/app/internal/store.
Probar responder esta pregunta con un coach de IA

Flujo de control

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.

package main

import "fmt"

func f() (result int) {
    x := 1
    defer fmt.Println("arg evaluated now:", x)
    defer func() {
        result++
        fmt.Println("deferred closure sees x later:", x)
    }()
    x = 2
    return 10
}

func main() {
    fmt.Println("return:", f())
}
Probar responder esta pregunta con un coach de IA

Manejo de errores

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 { ... }`.

package main

import (
	"errors"
	"fmt"
)

func findUser(id int) (string, error) {
	if id <= 0 {
		return "", errors.New("invalid user id")
	}
	if id == 42 {
		return "", fmt.Errorf("user %d not found", id)
	}
	return "alice", nil
}

func handler(id int) error {
	name, err := findUser(id)
	if err != nil {
		return fmt.Errorf("find user: %w", err)
	}
	fmt.Println(name)
	return nil
}
Probar responder esta pregunta con un coach de IA

16¿Cómo se debe gestionar la recuperación de pánicos (panic recovery) en servicios backend en Go, incluyendo qué sucede cuando una goroutine entra en pánico y cuándo un proceso debe recuperarse en lugar de cerrarse abruptamente?

Un pánico desenrolla la goroutine actual ejecutando sus funciones diferidas (`defer`). `recover` solo funciona cuando se invoca desde una función diferida en esa misma goroutine; una goroutine no puede recuperar el pánico de otra. Si un pánico no se recupera, el proceso se cierra abruptamente. En servicios backend, la recuperación generalmente debe ubicarse en los límites de aislamiento, como manejadores de solicitudes, middleware de RPC (Remote Procedure Call) o puntos de entrada de goroutines de trabajo, de modo que una solicitud o tarea fallida no derribe todo el servicio. Sin embargo, si un pánico pudo haber corrompido el estado compartido o comprometido la integridad del proceso, es más seguro permitir que el proceso falle y se reinicie en lugar de recuperarse y continuar a ciegas.

func Recover(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        defer func() {
            if rec := recover(); rec != nil {
                log.Printf("panic: %v\n%s", rec, debug.Stack())
                http.Error(w, "internal server error", http.StatusInternalServerError)
            }
        }()
        next.ServeHTTP(w, r)
    })
}
Probar responder esta pregunta con un coach de IA

Concurrencia

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.

var in <-chan int = source
for in != nil {
    select {
    case v, ok := <-in:
        if !ok {
            in = nil // disables this receive case
            continue
        }
        fmt.Println(v)
    case <-ctx.Done():
        return
    }
}
Probar responder esta pregunta con un coach de IA

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.

package main

import (
    "sync"
    "sync/atomic"
)

var requests atomic.Int64

func recordRequest() {
    requests.Add(1) // one independent counter update
}

type Account struct {
    mu      sync.Mutex
    balance int
    limit   int
}

func (a *Account) Withdraw(n int) bool {
    a.mu.Lock()
    defer a.mu.Unlock()

    // The check and update must be one protected invariant.
    if a.balance-n < -a.limit {
        return false
    }
    a.balance -= n
    return true
}
Probar responder esta pregunta con un coach de IA

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.

package worker

import (
    "context"
    "sync"
)

type Job int
type Result int

func StartWorkers(ctx context.Context, jobs <-chan Job, n int) <-chan Result {
    results := make(chan Result)

    var wg sync.WaitGroup
    wg.Add(n)
    for i := 0; i < n; i++ {
        go func() {
            defer wg.Done()
            for {
                select {
                case <-ctx.Done():
                    return
                case j, ok := <-jobs:
                    if !ok {
                        return
                    }
                    r := Result(j * 2)
                    select {
                    case results <- r:
                    case <-ctx.Done():
                        return
                    }
                }
            }
        }()
    }

    go func() {
        wg.Wait()
        close(results) // close after all senders are done
    }()

    return results
}
Probar responder esta pregunta con un coach de IA

20¿Cuáles son las causas comunes de fugas de goroutines en servicios Go y cómo se detectan y solucionan en producción?

Las fugas comunes de goroutines en servicios Go provienen de goroutines bloqueadas indefinidamente en envíos o recepciones de canales, esperando en otras operaciones bloqueantes sin cancelación, E/S bloqueada sin tiempos límite (deadlines), bucles en segundo plano o tickers que nunca se detienen, y goroutines con ámbito de solicitud que sobreviven a la propia solicitud. En producción, se busca un crecimiento sostenido en el recuento de goroutines y síntomas relacionados, y luego se inspeccionan volcados de goroutines o perfiles de goroutines con pprof para ver dónde están atascadas. Solucionar la fuga implica modificar el código para que esas goroutines puedan salir: añadir cancelación y tiempos límite, detener tickers, cerrar canales correctamente, evitar goroutines de solicitud desacopladas y limitar la concurrencia donde sea necesario.

func handler(w http.ResponseWriter, r *http.Request) {
    go func() {
        result := <-slowCh // may block forever
        _ = result
    }()
}
Probar responder esta pregunta con un coach de IA