# Top 25 preguntas de entrevista Go: guía completa para desarrolladores > Domina las entrevistas de Go con las 25 preguntas más frecuentes. Goroutines, channels, interfaces y patrones de concurrencia con ejemplos de código. - Published: 2026-02-06 - Updated: 2026-04-28 - Author: SharpSkill - Tags: go, golang, interview, concurrency, goroutines - Reading time: 18 min --- Las entrevistas técnicas de Go evalúan la comprensión de los conceptos fundamentales del lenguaje: concurrencia, gestión de memoria y patrones idiomáticos. Esta guía cubre las 25 preguntas más frecuentes con respuestas detalladas y ejemplos de código. > **Consejo para la entrevista** > > Go valora la simplicidad y la legibilidad. Los entrevistadores buscan respuestas concisas que demuestren comprensión profunda en lugar de soluciones excesivamente complejas. ## Fundamentos del lenguaje Go ### 1. ¿Cuál es la diferencia entre `var` y `:=`? La declaración `var` permite especificar el tipo explícitamente y funciona a nivel de paquete. El operador `:=` infiere el tipo de forma automática, pero solo se utiliza dentro de funciones. ```go // declaration.go package main // Package level - var required var globalConfig = "production" func main() { // var with explicit type var count int = 10 // var with type inference var name = "Alice" // Short declaration - functions only age := 25 // Multiple declarations var ( host = "localhost" port = 8080 ) } ``` La declaración corta `:=` se prefiere dentro de las funciones por concisión, mientras que `var` sigue siendo necesaria para variables a nivel de paquete. ### 2. ¿Cómo funciona el sistema de tipos de Go? Go utiliza tipado estático con inferencia de tipos. El lenguaje distingue entre tipos por valor (copiados al asignarlos) y tipos por referencia (que comparten la estructura subyacente). ```go // types.go package main import "fmt" func main() { // Value types - full copy a := [3]int{1, 2, 3} b := a // Copies the array b[0] = 100 // Doesn't modify a fmt.Println(a) // [1 2 3] // Reference types - share data slice1 := []int{1, 2, 3} slice2 := slice1 // Same underlying array slice2[0] = 100 // Also modifies slice1 fmt.Println(slice1) // [100 2 3] // Maps are also references m1 := map[string]int{"a": 1} m2 := m1 m2["a"] = 100 fmt.Println(m1["a"]) // 100 } ``` Los arrays son tipos por valor, mientras que los slices, los maps y los channels son tipos por referencia. ### 3. Explica la diferencia entre arrays y slices Los arrays tienen un tamaño fijo definido en tiempo de compilación. Los slices son vistas dinámicas sobre un array subyacente, con tres componentes: puntero, longitud y capacidad. ```go // arrays_slices.go package main import "fmt" func main() { // Array - fixed size, value type arr := [5]int{1, 2, 3, 4, 5} // Slice - view over the array slice := arr[1:4] // [2 3 4] fmt.Printf("len=%d, cap=%d\n", len(slice), cap(slice)) // len=3, cap=4 // Modifications affect original array slice[0] = 20 fmt.Println(arr) // [1 20 3 4 5] // Direct creation with make dynamic := make([]int, 3, 10) // len=3, cap=10 // Append may reallocate dynamic = append(dynamic, 1, 2, 3, 4, 5) } ``` Los slices son el tipo preferido para colecciones dinámicas en Go. ### 4. ¿Cómo funciona la sentencia `defer`? `defer` programa la ejecución de una llamada al final de la función que la contiene. Las llamadas diferidas se apilan y se ejecutan en orden LIFO (último en entrar, primero en salir). ```go // defer.go package main import ( "fmt" "os" ) func main() { // LIFO order defer fmt.Println("1") defer fmt.Println("2") defer fmt.Println("3") // Prints: 3, 2, 1 } // Typical use case: resource cleanup func readFile(path string) ([]byte, error) { file, err := os.Open(path) if err != nil { return nil, err } defer file.Close() // Always executes // Read file... return os.ReadFile(path) } // Caution: arguments are evaluated immediately func deferArgs() { x := 10 defer fmt.Println(x) // Captures 10 x = 20 // Prints: 10 } ``` `defer` garantiza la ejecución incluso durante un panic, lo que lo hace ideal para liberar recursos. ### 5. ¿Qué es una interfaz en Go? Una interfaz define un conjunto de métodos. Cualquier tipo que implemente esos métodos satisface la interfaz de forma implícita, sin necesidad de una declaración explícita. ```go // interfaces.go package main import "fmt" // Interface definition type Writer interface { Write([]byte) (int, error) } // Type that implicitly implements Writer type FileLogger struct { path string } func (f *FileLogger) Write(data []byte) (int, error) { // Write to file fmt.Println("Writing to", f.path) return len(data), nil } // Empty interface - accepts any type func printAny(v interface{}) { fmt.Printf("Type: %T, Value: %v\n", v, v) } // Type assertion func process(w Writer) { // Type check if fl, ok := w.(*FileLogger); ok { fmt.Println("FileLogger with path:", fl.path) } } ``` La implementación implícita de interfaces permite un fuerte desacoplamiento entre paquetes. ## Concurrencia y goroutines ### 6. ¿Qué es una goroutine y en qué se diferencia de un thread? Una goroutine es un hilo ligero gestionado por el runtime de Go. Usa unos pocos KB de stack (frente a varios MB de un hilo del sistema) y el scheduler de Go multiplexa miles de goroutines sobre unos pocos hilos del sistema. ```go // goroutines.go package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup // Launch 1000 goroutines for i := 0; i < 1000; i++ { wg.Add(1) go func(id int) { defer wg.Done() time.Sleep(100 * time.Millisecond) fmt.Printf("Goroutine %d finished\n", id) }(i) // Pass i by value } wg.Wait() fmt.Println("All goroutines completed") } ``` > **Trampa habitual** > > Conviene pasar siempre por valor las variables de bucle a las goroutines. De lo contrario, todas pueden capturar el mismo valor final. ### 7. Explica cómo funcionan los channels Los channels permiten la comunicación y la sincronización entre goroutines. Pueden ser con búfer (con capacidad) o sin búfer (síncronos). ```go // channels.go package main import "fmt" func main() { // Unbuffered channel - blocks until received ch := make(chan int) go func() { ch <- 42 // Blocks until read }() value := <-ch // Receives value fmt.Println(value) // Buffered channel - doesn't block until full buffered := make(chan string, 2) buffered <- "first" buffered <- "second" // buffered <- "third" // Would block fmt.Println(<-buffered) // "first" fmt.Println(<-buffered) // "second" } ``` Los channels sin búfer garantizan la sincronización, mientras que los channels con búfer permiten un desacoplamiento temporal. ### 8. ¿Cómo se usa `select` con varios channels? `select` espera de forma simultánea sobre varias operaciones de channel. Se ejecuta la primera que esté lista, eligiendo aleatoriamente en caso de empate. ```go // select.go package main import ( "fmt" "time" ) func main() { ch1 := make(chan string) ch2 := make(chan string) go func() { time.Sleep(100 * time.Millisecond) ch1 <- "from ch1" }() go func() { time.Sleep(200 * time.Millisecond) ch2 <- "from ch2" }() // Wait with timeout for i := 0; i < 2; i++ { select { case msg := <-ch1: fmt.Println(msg) case msg := <-ch2: fmt.Println(msg) case <-time.After(500 * time.Millisecond): fmt.Println("Timeout") } } // Non-blocking select with default select { case msg := <-ch1: fmt.Println(msg) default: fmt.Println("No message available") } } ``` `select` es la herramienta fundamental para gestionar la concurrencia en Go con elegancia. ### 9. ¿Cómo se evitan las race conditions? Las race conditions ocurren cuando varias goroutines acceden a datos compartidos sin sincronización. Go ofrece varios mecanismos de protección. ```go // race_conditions.go package main import ( "fmt" "sync" "sync/atomic" ) // Solution 1: Mutex type SafeCounter struct { mu sync.Mutex count int } func (c *SafeCounter) Increment() { c.mu.Lock() defer c.mu.Unlock() c.count++ } // Solution 2: RWMutex for read-heavy workloads type Cache struct { mu sync.RWMutex data map[string]string } func (c *Cache) Get(key string) string { c.mu.RLock() // Multiple readers allowed defer c.mu.RUnlock() return c.data[key] } func (c *Cache) Set(key, value string) { c.mu.Lock() // Single writer defer c.mu.Unlock() c.data[key] = value } // Solution 3: atomic for simple counters var atomicCounter int64 func incrementAtomic() { atomic.AddInt64(&atomicCounter, 1) } func main() { // Detection: go run -race main.go counter := SafeCounter{} var wg sync.WaitGroup for i := 0; i < 1000; i++ { wg.Add(1) go func() { defer wg.Done() counter.Increment() }() } wg.Wait() fmt.Println("Count:", counter.count) } ``` El flag `-race` del compilador detecta race conditions en tiempo de ejecución. ### 10. Explica el patrón worker pool El patrón worker pool limita la concurrencia creando un número fijo de goroutines que procesan tareas desde una cola. ```go // worker_pool.go package main import ( "fmt" "sync" "time" ) func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) { defer wg.Done() for job := range jobs { fmt.Printf("Worker %d processing job %d\n", id, job) time.Sleep(100 * time.Millisecond) // Simulate work results <- job * 2 } } func main() { const numJobs = 10 const numWorkers = 3 jobs := make(chan int, numJobs) results := make(chan int, numJobs) var wg sync.WaitGroup // Start workers for w := 1; w <= numWorkers; w++ { wg.Add(1) go worker(w, jobs, results, &wg) } // Send jobs for j := 1; j <= numJobs; j++ { jobs <- j } close(jobs) // Wait and close results go func() { wg.Wait() close(results) }() // Collect results for result := range results { fmt.Println("Result:", result) } } ``` Este patrón evita la sobrecarga de memoria y CPU asociada a crear demasiadas goroutines. ## Manejo de errores y panic/recover ### 11. ¿Cómo se gestionan los errores en Go? Go usa valores de retorno explícitos para los errores, sin excepciones. Por convención, error es el último parámetro retornado. ```go // errors.go package main import ( "errors" "fmt" ) // Sentinel errors for comparison var ( ErrNotFound = errors.New("resource not found") ErrUnauthorized = errors.New("access unauthorized") ) // Custom error type type ValidationError struct { Field string Message string } func (e *ValidationError) Error() string { return fmt.Sprintf("validation %s: %s", e.Field, e.Message) } func validateAge(age int) error { if age < 0 { return &ValidationError{ Field: "age", Message: "must be positive", } } return nil } func main() { // Basic check if err := validateAge(-5); err != nil { // Type assertion for custom error var valErr *ValidationError if errors.As(err, &valErr) { fmt.Printf("Field: %s\n", valErr.Field) } } // Sentinel error comparison err := findUser("unknown") if errors.Is(err, ErrNotFound) { fmt.Println("User not found") } } func findUser(id string) error { // Error wrapping with context return fmt.Errorf("findUser %s: %w", id, ErrNotFound) } ``` Envolver con `%w` encadena los errores manteniendo la posibilidad de comprobar el error original. ### 12. ¿Cuándo conviene usar panic y recover? `panic` interrumpe el flujo normal y desenrolla la pila. `recover` captura el panic en un `defer` y permite que la ejecución continúe. ```go // panic_recover.go package main import "fmt" func safeOperation() (err error) { defer func() { if r := recover(); r != nil { err = fmt.Errorf("recovered from panic: %v", r) } }() riskyOperation() return nil } func riskyOperation() { // Simulates an operation that can panic panic("something went wrong") } // Legitimate use case: initialization validation func MustCompileRegex(pattern string) *Regexp { r, err := regexp.Compile(pattern) if err != nil { panic(err) // Programming error } return r } func main() { err := safeOperation() if err != nil { fmt.Println("Recovered error:", err) } fmt.Println("Program continues") } ``` > **Regla de oro** > > Usa panic solo para errores de programación (invariantes violadas). Para errores esperados (archivo ausente, fallos de red), conviene siempre devolver un error. ## Structs, métodos y embedding ### 13. ¿Qué diferencia hay entre receivers por valor y por puntero? Un receiver por valor recibe una copia del struct, mientras que un receiver por puntero recibe una referencia y puede modificar el original. ```go // receivers.go package main import "fmt" type Counter struct { value int } // Value receiver - works on copy func (c Counter) GetValue() int { return c.value } // Pointer receiver - modifies original func (c *Counter) Increment() { c.value++ } // Pointer receiver for large structs (avoids copy) type LargeStruct struct { data [1000]int } func (l *LargeStruct) Process() { // Avoids copying 8000 bytes } func main() { c := Counter{value: 0} c.Increment() // Go automatically converts fmt.Println(c.GetValue()) // 1 // Careful with interfaces var _ fmt.Stringer = &c // OK if method on *Counter } ``` Regla: si un método usa receiver por puntero, todos los métodos del tipo deberían usar receiver por puntero por coherencia. ### 14. ¿Cómo funciona el embedding en Go? El embedding incluye un tipo dentro de otro, heredando sus métodos y campos. No es herencia clásica, sino composición. ```go // embedding.go package main import "fmt" type Logger struct { prefix string } func (l *Logger) Log(msg string) { fmt.Printf("[%s] %s\n", l.prefix, msg) } // Embedding Logger type Service struct { *Logger // Pointer embedding name string } func NewService(name string) *Service { return &Service{ Logger: &Logger{prefix: name}, name: name, } } func main() { svc := NewService("API") // Promoted method - direct access svc.Log("Starting") // Explicit access also works svc.Logger.Log("Explicit") // Promoted field fmt.Println(svc.prefix) // "API" } ``` El embedding permite composiciones flexibles evitando la rigidez de la herencia. ### 15. ¿Cómo se implementa el patrón singleton en Go? El paquete `sync` ofrece `sync.Once` para garantizar una única ejecución de la inicialización, incluso con goroutines concurrentes. ```go // singleton.go package main import ( "fmt" "sync" ) type Database struct { connectionString string } var ( instance *Database once sync.Once ) func GetDatabase() *Database { once.Do(func() { fmt.Println("Single initialization") instance = &Database{ connectionString: "postgres://...", } }) return instance } func main() { // Concurrent calls - single initialization var wg sync.WaitGroup for i := 0; i < 10; i++ { wg.Add(1) go func() { defer wg.Done() db := GetDatabase() fmt.Printf("Instance: %p\n", db) }() } wg.Wait() } ``` `sync.Once` es seguro para concurrencia y más elegante que un mutex con double-check locking. ## Context y cancelación ### 16. ¿Para qué sirve el paquete context? El paquete `context` gestiona deadlines, señales de cancelación y valores asociados a una request a lo largo del árbol de llamadas. ```go // context.go package main import ( "context" "fmt" "time" ) func main() { // Context with timeout ctx, cancel := context.WithTimeout( context.Background(), 2*time.Second, ) defer cancel() // Always call cancel result := make(chan string, 1) go func() { // Simulate long operation time.Sleep(3 * time.Second) result <- "completed" }() select { case res := <-result: fmt.Println(res) case <-ctx.Done(): fmt.Println("Timeout:", ctx.Err()) } } // Propagation through functions func fetchData(ctx context.Context, url string) ([]byte, error) { // Early check if ctx.Err() != nil { return nil, ctx.Err() } req, err := http.NewRequestWithContext(ctx, "GET", url, nil) if err != nil { return nil, err } // HTTP client respects context resp, err := http.DefaultClient.Do(req) // ... } ``` Toda función potencialmente larga debería aceptar un `context.Context` como primer parámetro. ### 17. ¿Cómo se gestiona un graceful shutdown del programa? Las señales del sistema, como SIGINT y SIGTERM, pueden capturarse para permitir un cierre limpio. ```go // graceful_shutdown.go package main import ( "context" "fmt" "os" "os/signal" "syscall" "time" ) func main() { // Context cancelled on signal ctx, stop := signal.NotifyContext( context.Background(), syscall.SIGINT, syscall.SIGTERM, ) defer stop() // Start server server := startServer() // Wait for signal <-ctx.Done() fmt.Println("\nShutting down...") // Timeout for graceful shutdown shutdownCtx, cancel := context.WithTimeout( context.Background(), 5*time.Second, ) defer cancel() if err := server.Shutdown(shutdownCtx); err != nil { fmt.Println("Shutdown error:", err) } fmt.Println("Shutdown complete") } ``` Este patrón asegura que las conexiones activas terminen correctamente antes del cierre. ## Tests y benchmarks ### 18. ¿Cómo se escriben tests en Go? El paquete `testing` integrado proporciona la funcionalidad básica. Los tests se ubican en archivos `*_test.go`. ```go // calculator_test.go package calculator import "testing" func TestAdd(t *testing.T) { result := Add(2, 3) if result != 5 { t.Errorf("Add(2, 3) = %d; want 5", result) } } // Table-driven tests func TestAddTableDriven(t *testing.T) { tests := []struct { name string a, b int expected int }{ {"positive", 2, 3, 5}, {"negative", -1, -1, -2}, {"mixed", -1, 5, 4}, {"zero", 0, 0, 0}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := Add(tt.a, tt.b) if result != tt.expected { t.Errorf("Add(%d, %d) = %d; want %d", tt.a, tt.b, result, tt.expected) } }) } } ``` Los table-driven tests son el patrón idiomático en Go para probar varios casos. ### 19. ¿Cómo se escriben benchmarks? Los benchmarks usan `testing.B` y se ejecutan con `go test -bench`. ```go // benchmark_test.go package main import ( "strings" "testing" ) func BenchmarkStringConcat(b *testing.B) { for i := 0; i < b.N; i++ { var s string for j := 0; j < 100; j++ { s += "a" } } } func BenchmarkStringBuilder(b *testing.B) { for i := 0; i < b.N; i++ { var sb strings.Builder for j := 0; j < 100; j++ { sb.WriteString("a") } _ = sb.String() } } // Typical results: // BenchmarkStringConcat-8 50000 28000 ns/op // BenchmarkStringBuilder-8 1000000 1200 ns/op ``` Los benchmarks revelan diferencias de rendimiento entre implementaciones. ## Generics (Go 1.18+) ### 20. ¿Cómo se usan los generics en Go? Go 1.18 introdujo los parámetros de tipo, lo que permite escribir código genérico manteniendo la seguridad de tipos. ```go // generics.go package main import "fmt" // Generic function func Map[T, U any](slice []T, fn func(T) U) []U { result := make([]U, len(slice)) for i, v := range slice { result[i] = fn(v) } return result } // Custom type constraint type Number interface { int | int64 | float64 } func Sum[T Number](values []T) T { var sum T for _, v := range values { sum += v } return sum } // Generic type type Stack[T any] struct { items []T } func (s *Stack[T]) Push(item T) { s.items = append(s.items, item) } func (s *Stack[T]) Pop() (T, bool) { if len(s.items) == 0 { var zero T return zero, false } item := s.items[len(s.items)-1] s.items = s.items[:len(s.items)-1] return item, true } func main() { // Usage doubled := Map([]int{1, 2, 3}, func(n int) int { return n * 2 }) fmt.Println(doubled) // [2 4 6] fmt.Println(Sum([]int{1, 2, 3, 4, 5})) // 15 stack := &Stack[string]{} stack.Push("hello") stack.Push("world") val, _ := stack.Pop() fmt.Println(val) // "world" } ``` Los generics eliminan la necesidad de duplicar código o de usar `interface{}`. ## Módulos y dependencias ### 21. ¿Cómo funciona el sistema de módulos de Go? Los módulos de Go gestionan las dependencias con versionado semántico. El archivo `go.mod` define el módulo y sus dependencias. ```go // go.mod example module github.com/user/myproject go 1.21 require ( github.com/gin-gonic/gin v1.9.1 github.com/lib/pq v1.10.9 ) // Essential commands: // go mod init github.com/user/project // go mod tidy - clean dependencies // go get package@v1.2.3 - add/update // go mod vendor - copy locally ``` ```bash # Updating dependencies go get -u ./... # All dependencies go get -u=patch ./... # Patches only ``` El archivo `go.sum` contiene los checksums criptográficos para garantizar la integridad de las dependencias. ### 22. ¿Cómo conviene estructurar un proyecto en Go? La estructura estándar sigue convenciones de la comunidad sin imponer reglas estrictas. ``` myproject/ ├── cmd/ │ └── api/ │ └── main.go # Entry point ├── internal/ # Private to module │ ├── handler/ │ ├── service/ │ └── repository/ ├── pkg/ # Reusable external code ├── go.mod ├── go.sum └── README.md ``` La carpeta `internal` es especial: su contenido no puede ser importado por otros módulos. ## Preguntas avanzadas ### 23. ¿Cómo funciona el garbage collector en Go? Go utiliza un garbage collector concurrente, tricolor mark-and-sweep, optimizado para baja latencia. ```go // gc_optimization.go package main import "runtime" func main() { // GC configuration // GOGC=100 (default) - triggers GC when heap doubles // Force GC runtime.GC() // Memory statistics var stats runtime.MemStats runtime.ReadMemStats(&stats) println("Alloc:", stats.Alloc) println("NumGC:", stats.NumGC) println("PauseTotalNs:", stats.PauseTotalNs) } // Optimization techniques // 1. Reuse allocations with sync.Pool // 2. Pre-allocate slices with make([]T, 0, cap) // 3. Avoid repeated string/[]byte conversions // 4. Use pointers for large structs ``` La variable de entorno `GODEBUG=gctrace=1` muestra los trazos del GC. ### 24. Explica el scheduler de Go El scheduler de Go usa un modelo M:N que mapea N goroutines sobre M hilos del sistema, con tres entidades: G (goroutine), M (thread) y P (procesador lógico). ```go // scheduler.go package main import ( "fmt" "runtime" ) func main() { // Number of logical processors (P) fmt.Println("GOMAXPROCS:", runtime.GOMAXPROCS(0)) // Number of active goroutines fmt.Println("NumGoroutine:", runtime.NumGoroutine()) // Yield processor to other goroutines runtime.Gosched() // M:P:G model // - G: goroutine (lightweight stack ~2KB) // - M: OS thread (machine) // - P: logical processor (execution context) // // Each P has a local queue of Gs // Work stealing when queue is empty } ``` El scheduler es preemptivo desde Go 1.14, lo que evita que una goroutine monopolice un P. ### 25. ¿Cómo se optimiza el rendimiento en Go? La optimización empieza por hacer profiling para identificar cuellos de botella. ```go // profiling.go package main import ( "os" "runtime/pprof" ) func main() { // CPU profiling f, _ := os.Create("cpu.prof") pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() // Code to profile... // Memory profiling mf, _ := os.Create("mem.prof") defer mf.Close() pprof.WriteHeapProfile(mf) } // Analysis: go tool pprof cpu.prof // Common optimization techniques: // 1. Avoid allocations in hot loops // 2. Use sync.Pool for reusable objects // 3. Prefer []byte over string for mutations // 4. Use bufio for I/O // 5. Batch database operations ``` > **Regla de optimización** > > Conviene medir antes de optimizar. El profiling suele revelar sorpresas sobre los cuellos de botella reales. ## Conclusión Estas 25 preguntas cubren los conceptos fundamentales evaluados en las entrevistas de Go: **Checklist de preparación:** - ✅ Dominio de las goroutines y los channels - ✅ Comprensión de las interfaces implícitas - ✅ Manejo idiomático de errores - ✅ Uso adecuado de context - ✅ Patrones de concurrencia (mutex, worker pool) - ✅ Tests y benchmarks - ✅ Conocimiento de los generics de Go 1.18+ La clave del éxito en una entrevista de Go: demostrar comprensión de los compromisos entre simplicidad y rendimiento, y saber cuándo aplicar cada patrón de concurrencia. --- Source: SharpSkill (https://sharpskill.dev), tech interview preparation for your real stack. HTML version of this page: https://sharpskill.dev/es/blog/go/top-25-go-interview-questions