Swift y Go: simplicidad contra expresividad
Un lenguaje que cabe en una tarde frente a otro que no cabe en un año. Qué compra Go con su austeridad deliberada, qué compra Swift con su superficie enorme, y cómo esa decisión inicial se propaga hasta el modelo de concurrencia y el tratamiento de los errores.
Go es el único lenguaje popular de los últimos veinte años cuyo principal argumento de venta es lo que no tiene. No hay herencia, no hay excepciones, no hay sobrecarga de operadores, no hay macros, no hubo genéricos durante trece años y su especificación completa se lee en una tarde. Swift, en el extremo opuesto, acumula protocolos con tipos asociados, tipos opacos y existenciales, envoltorios de propiedad, constructores de resultados, macros y un sistema de concurrencia con aislamiento verificado. Se les compara constantemente como si compitieran por lo mismo, y casi nunca compiten: uno optimiza el coste de que mil personas mantengan un servicio durante diez años, el otro optimiza el poder de expresar un dominio con precisión. Todo lo demás —errores, concurrencia, tiempos de compilación— se sigue de ahí.
- Explicar la austeridad de
Gocomo decisión de ingeniería organizativa, no como limitación técnica. - Contrastar el modelo de comunicación por canales con el modelo de aislamiento por actores.
- Analizar el error como valor frente al error como canal aparte, con sus costes respectivos.
- Situar en qué dominios la elección entre ambos lenguajes está prácticamente decidida de antemano.
Dos filosofías del tamaño
La tesis de Go es que el cuello de botella del software grande no es escribir código sino leerlo, y que un lenguaje pequeño produce código legible incluso escrito por gente mediocre o con prisa. De ahí la ausencia deliberada de abstracciones potentes: cuando solo hay una forma obvia de hacer algo, cualquiera puede leer el código de cualquiera sin descifrar primero el dialecto personal del autor. El formateo canónico obligatorio, la compilación en segundos y el rechazo histórico a los genéricos son manifestaciones de la misma tesis.
La tesis de Swift es la contraria: si el lenguaje permite expresar los invariantes del dominio en el sistema de tipos, el compilador se convierte en un revisor incansable y una clase entera de errores desaparece antes de ejecutarse. De ahí los enumerados con valores asociados que hacen imposible representar un estado inválido, la exhaustividad obligatoria, la distinción entre valor y referencia y la maquinaria genérica para escribir una vez algoritmos que sirvan para muchos tipos.
type Estado struct {
Cargando bool
Datos []Item
Err error
}
// nada impide Cargando true con Err no nulo: el tipo admite estados imposibles
enum Estado {
case cargando
case cargado([Item])
case fallo(Error)
}
// los estados imposibles no se pueden ni escribir
El precio de cada postura es simétrico y conocido. Go genera repetición —el mismo bucle de comprobación mil veces— y empuja la corrección hacia las pruebas y la revisión humana. Swift genera una curva de aprendizaje larga, diagnósticos difíciles cuando la inferencia falla y tiempos de compilación que en proyectos grandes se miden en minutos, no en segundos.
Concurrencia: comunicar frente a aislar
Ambos lenguajes tienen concurrencia de primera clase y ambos evitan que el programador toque hilos del sistema, pero parten de axiomas distintos. Go sigue el modelo de procesos secuenciales comunicantes: lanza rutinas baratísimas y las coordina pasando valores por canales, bajo el lema de compartir memoria comunicando en lugar de comunicar compartiendo memoria. Swift sigue el modelo de aislamiento: el estado vive dentro de actores, el compilador verifica qué puede cruzar de un dominio a otro y las tareas forman un árbol donde la cancelación y los errores se propagan solos.
func cargar(ids []int) []Perfil {
ch := make(chan Perfil, len(ids))
for _, id := range ids {
go func(i int) { ch <- api.Perfil(i) }(id) // sin arbol, sin padre
}
out := make([]Perfil, 0, len(ids))
for range ids { out = append(out, <-ch) }
return out
}
func cargar(ids: [Int]) async throws -> [Perfil] {
try await withThrowingTaskGroup(of: Perfil.self) { grupo in
for id in ids { grupo.addTask { try await api.perfil(id) } }
return try await grupo.reduce(into: []) { $0.append($1) }
}
}
Las dos diferencias que importan no se ven en el fragmento. La primera es que la rutina de Go no tiene padre: nadie la espera, nadie la cancela salvo que le pases un contexto a mano, y la fuga de rutinas es un modo de fallo cotidiano que ninguna herramienta detecta en compilación. En Swift la tarea hija no puede sobrevivir a su ámbito, y esa es toda la diferencia entre concurrencia estructurada y concurrencia a secas. La segunda es más grave: Go permite carreras de datos. Su modelo de memoria las declara comportamiento indefinido, pero el compilador no las impide; existe un detector que las descubre en ejecución, si tienes la suerte de que la prueba las provoque. Swift 6 las rechaza antes de compilar.
Lanzar una rutina en Go cuesta unos pocos kilobytes de pila y es tentador hacerlo sin pensar. Ese bajo coste unitario es justamente lo que hace habituales las fugas: nada te obliga a saber cuándo termina lo que lanzaste. La concurrencia estructurada encarece ligeramente la sintaxis a cambio de eliminar esa clase entera de fallos.
Errores: valores frente a canal aparte
Go trata el error como un valor de retorno más. No hay excepciones, no hay desenrollado de pila, y la comprobación es explícita hasta la fatiga.
f, err := os.Open(ruta)
if err != nil { return fmt.Errorf("abriendo %s: %w", ruta, err) }
defer f.Close()
Swift lo trata como un canal aparte que viaja fuera del tipo de retorno pero exige marcar cada punto de propagación con try, de modo que el lector sigue viendo dónde puede torcerse el flujo sin tener que escribir la comprobación.
func leer(_ ruta: String) throws(ErrorArchivo) -> Datos {
let f = try abrir(ruta)
defer { f.cerrar() }
return try f.leerTodo()
}
| Aspecto | Go |
Swift |
|---|---|---|
| Representación | Valor devuelto | Canal separado con try |
| Ignorarlo | Posible asignando a _ |
Imposible sin escribirlo |
| Tipo del error | Interfaz abierta | Protocolo abierto o tipado con throws tipado |
| Contexto | Envoltura con %w e inspección posterior |
Jerarquía de tipos y switch exhaustivo |
| Caso catastrófico | panic y recover |
fatalError sin captura posible |
La crítica clásica a Go es la repetición; la crítica menos citada y más seria es que nada obliga a manejar el error, y que la envoltura por texto formateado convierte la inspección en una operación de cadena disfrazada de tipo. La crítica a Swift es inversa: el canal aparte hace que el error sea invisible en la firma salvo por una palabra, y hasta la llegada del throws tipado no había forma de saber qué podía lanzarse sin leer la implementación.
Lo que cada uno se lleva a producción
Go produce un único binario estático, se compila cruzado a cualquier plataforma cambiando dos variables de entorno, arranca instantáneamente y tiene detrás el ecosistema que construyó la infraestructura de la nube. Su recolector de basura sacrifica rendimiento bruto por pausas mínimas, una elección perfecta para servicios de red y pésima para bucles de audio o gráficos.
Swift produce binarios que dependen de un tiempo de ejecución y una ABI, aunque el enlazado estático y el kit multiplataforma para Linux han cerrado buena parte de esa brecha. A cambio ofrece rendimiento predecible sin pausas, control real sobre la representación de los datos y un sistema de tipos con el que se pueden modelar dominios complejos. En el servidor ambos son viables; la elección casi nunca la decide el lenguaje sino la disponibilidad de bibliotecas y de gente que ya los conozca.
flowchart TD Q[Que optimiza el lenguaje] --> G[Go optimiza legibilidad a escala] Q --> S[Swift optimiza expresividad del dominio] G --> G1[Superficie minima y compilacion rapida] G --> G2[Canales y rutinas sin arbol] G --> G3[Error como valor devuelto] S --> S1[Sistema de tipos amplio] S --> S2[Actores y tareas estructuradas] S --> S3[Error como canal marcado con try] G2 --> R[Carreras posibles, detector en ejecucion] S2 --> V[Carreras rechazadas en compilacion] style R fill:#f38ba8,color:#11111b style V fill:#a6e3a1,color:#11111b
Menos lenguaje, más código
Go reduce lo que hay que aprender y aumenta lo que hay que escribir. Swift hace exactamente lo contrario.
La tarea tiene padre
Toda tarea de Swift vive dentro de un ámbito que la espera y la cancela. Una rutina de Go no.
Detectar o impedir
Go descubre carreras ejecutando con el detector. Swift 6 las convierte en errores de compilación.
La austeridad de Go suele defenderse y atacarse como una cuestión estética, y es un error de categoría: se trata de una hipótesis falsable sobre economía organizativa, formulada por gente que había visto compilaciones de una hora y jerarquías de plantillas ininteligibles, y que decidió que el coste dominante del software industrial es la comprensión ajena y no la elegancia propia. Bajo esa hipótesis, retirar expresividad del lenguaje es racional aunque produzca repetición, porque la repetición es local y comprensible mientras que la abstracción mal elegida es global y contagiosa. La objeción seria no es que Go sea pobre, sino que la complejidad de un problema es una cantidad conservada: si el lenguaje no la absorbe, la absorben el código, las convenciones, las herramientas externas y las pruebas. Cuando un lenguaje carece de tipos suma, los estados imposibles no desaparecen, simplemente dejan de ser detectables por el compilador y pasan a vigilarse en revisión humana; cuando carece de concurrencia estructurada, las fugas de rutinas no desaparecen, pasan a vigilarse en producción con métricas. La postura de Swift es la apuesta contraria y tiene su propia factura, igual de real: cada construcción añadida amplía el espacio de dialectos posibles, y un equipo que use tipos opacos, macros y envoltorios de propiedad produce código que otro equipo del mismo lenguaje no sabe leer, además de diagnósticos que exigen entender el algoritmo de inferencia para descifrarlos. La conclusión útil no es elegir bando sino aprender a leer la factura: ante cualquier lenguaje, pregúntate qué complejidad ha decidido no modelar y adónde la ha empujado, porque siempre está en alguna parte, y el sitio donde acaba determina quién la paga, cuándo se descubre y cuánto cuesta arreglarla.
Go sacrifica expresividad para minimizar el coste de leer y mantener código a escala; Swift la maximiza para que el compilador verifique invariantes del dominio. Las rutinas son baratas pero huérfanas y admiten carreras que solo se detectan en ejecución; las tareas de Swift son estructuradas y su seguridad se verifica al compilar. El error como valor obliga a comprobar sin garantizar que lo hagas; el error como canal marcado con try obliga a manejarlo o propagarlo.
- Modela en
Goun estado con cuatro casos mutuamente excluyentes y cuenta cuántos estados inválidos admite el tipo resultante. - Escribe una función que lance tres rutinas y provoca deliberadamente una fuga; después reprodúcela con
withTaskGroupy explica por qué no puede fugarse. - Activa el detector de carreras sobre un contador compartido y describe por qué necesita que la prueba provoque el entrelazado.
- Convierte una cadena de errores envueltos con formato en una jerarquía de tipos de
Swiftconthrowstipado y compara qué puede decidir cada versión. - Argumenta con un caso real de tu trabajo cuál de los dos lenguajes elegirías, nombrando la restricción que decide y no la preferencia.