// 02 · el sistema de tipos
Tipos, structs e interfaces
Aquí vive el cambio mental más grande viniendo de Java: sin clases, sin herencia, sin excepciones — y sin extrañarlas.
// composición > herencia
Structs, métodos e interfaces
Go no tiene clases ni herencia. Tiene structs (datos), métodos (funciones con receptor) e interfaces que se cumplen solas.
// Un struct es solo datos. Mayúscula inicial = público (exportado),
// minúscula = privado al paquete. Así de simple es la visibilidad en Go.
type Usuario struct {
ID int
Nombre string
email string // privado: solo visible dentro de este paquete
}
// "Constructor": una función normal con la convención NewXxx
func NewUsuario(nombre, email string) *Usuario {
return &Usuario{Nombre: nombre, email: email}
}
// Método = función con RECEPTOR. Esto es lo más parecido a un método de clase.
// Receptor por valor (u Usuario): recibe una copia, no puede mutar.
func (u Usuario) Saludo() string {
return "Hola, " + u.Nombre
}
// Receptor por puntero (u *Usuario): puede modificar el original.
func (u *Usuario) CambiarEmail(nuevo string) {
u.email = nuevo
}Regla práctica receptor valor vs puntero¿El método muta el struct, o el struct es grande? → puntero. ¿Es de solo lectura y pequeño? → valor. Si dudas: usa puntero y sé consistente en todo el tipo. Es la misma decisión que pasar por copia vs por referencia, pero explícita.
Detalles que te van a preguntar: comparación, copia y struct tags
- Los structs se comparan con
==si todos sus campos son comparables — sin escribir equals/hashCode. Y sirven como clave de map. - Asignar un struct lo copia entero (semántica de valor, como los primitivos de Java).
b := a; b.Nombre = "otro"no tocaa. Para compartir, usa puntero. - Struct tags — los strings tras los campos (
`json:"nombre" db:"nombre"`) son metadatos que leen las librerías por reflexión: tu única "anotación" en Go, y solo para serialización/mapeo.
Embedding: composición en lugar de extends
type Auditoria struct {
CreadoEn time.Time
ActualizadoEn time.Time
}
func (a *Auditoria) Tocar() { a.ActualizadoEn = time.Now() }
// "Embeber" un struct: sus campos y métodos se PROMOCIONAN al contenedor.
// Parece herencia, pero es composición: no hay polimorfismo de subtipo,
// no hay super(), no hay jerarquías frágiles.
type Producto struct {
Auditoria // ← embedding (sin nombre de campo)
Nombre string
Precio float64
}
func main() {
p := Producto{Nombre: "Teclado", Precio: 899}
p.Tocar() // método promovido desde Auditoria
fmt.Println(p.ActualizadoEn) // campo promovido
}Interfaces implícitas: el giro que lo cambia todo
En Java escribes class EmailSender implements Notifier. En Go, nadie declara nada: si un tipo tiene los métodos que pide la interfaz, la cumple automáticamente. Se llama y es duck typing de Python, pero verificado por el compilador.
// La interfaz se define donde se USA (el consumidor), no donde se implementa.
type Notificador interface {
Enviar(destino, msg string) error
}
// Estos dos tipos NO saben que existe Notificador... y aun así lo cumplen.
type EmailSender struct{ smtp string }
func (e EmailSender) Enviar(destino, msg string) error {
fmt.Println("email a", destino, ":", msg)
return nil
}
type SMSSender struct{ apiKey string }
func (s SMSSender) Enviar(destino, msg string) error {
fmt.Println("sms a", destino, ":", msg)
return nil
}
// Polimorfismo clásico, sin jerarquías:
func avisarATodos(n Notificador, usuarios []string) {
for _, u := range usuarios {
n.Enviar(u, "¡Nueva versión disponible!")
}
}
func main() {
avisarATodos(EmailSender{smtp: "smtp.gmail.com"}, []string{"ana", "luis"})
avisarATodos(SMSSender{apiKey: "xyz"}, []string{"ana"})
}La inversión respecto a JavaEn Spring defines la interfaz junto al servicio que la implementa. En Go es al revés: el paquete que consume define la interfaz mínima que necesita. Por eso las interfaces Go son diminutas — las más famosas de la stdlib (io.Reader, io.Writer) tienen un solo método, y medio ecosistema encaja con ellas.
Type assertions, type switch y el tipo any
any (alias de interface{}) es el Object de Go: acepta cualquier cosa. Para recuperar el tipo concreto:
var v any = "hola"
s, ok := v.(string) // assertion segura: ok=false si no es string
if ok {
fmt.Println(len(s))
}
// type switch: el instanceof en cadena, pero legible
switch x := v.(type) {
case string:
fmt.Println("texto de", len(x), "bytes")
case int, int64:
fmt.Println("entero:", x)
case nil:
fmt.Println("sin valor")
default:
fmt.Printf("tipo inesperado %T\n", x)
}Desde que existen generics, any como parámetro es cada vez menos necesario. Si te encuentras usándolo mucho, probablemente hay un diseño mejor.
// sin try, sin catch, sin sorpresas
Errores como valores
Go no tiene excepciones para el flujo normal. Un error es un valor ordinario que las funciones devuelven y tú decides manejar o propagar. La firma es la documentación: si devuelve error, puede fallar.
error es simplemente una interfaz de un método: Error() string. El famoso if err != nil es el precio de que ningún error pueda ignorarse en silencio — piensa en checked exceptions de Java, pero sin la ceremonia de try/catch ni las jerarquías de RuntimeException.
import (
"errors"
"fmt"
)
// Error centinela: un valor exportado que otros pueden comparar
var ErrSaldoInsuficiente = errors.New("saldo insuficiente")
// Error con datos propios: un struct que implementa Error() string
type ErrorValidacion struct {
Campo string
Regla string
}
func (e *ErrorValidacion) Error() string {
return fmt.Sprintf("campo %q viola la regla %q", e.Campo, e.Regla)
}
func retirar(cuenta *Cuenta, monto float64) error {
if monto <= 0 {
return &ErrorValidacion{Campo: "monto", Regla: "positivo"}
}
if cuenta.Saldo < monto {
// %w ENVUELVE el error: conserva la causa original en la cadena
return fmt.Errorf("retirando %.2f: %w", monto, ErrSaldoInsuficiente)
}
cuenta.Saldo -= monto
return nil
}
func main() {
err := retirar(cuenta, 5000)
// errors.Is: ¿esta cadena de errores contiene el centinela?
if errors.Is(err, ErrSaldoInsuficiente) {
fmt.Println("ofrece un préstamo :)")
}
// errors.As: ¿hay un *ErrorValidacion en la cadena? extráelo
var ev *ErrorValidacion
if errors.As(err, &ev) {
fmt.Println("corrige el campo:", ev.Campo)
}
}Traducción directaerrors.Is ≈ catch (SaldoInsuficienteException e) · errors.As ≈ instanceof + cast · fmt.Errorf("...: %w", err) ≈ new ServiceException("...", cause). Todo lo que hacías con jerarquías de excepciones se hace con valores y estas dos funciones.
¿Y panic/recover? Cuándo sí (casi nunca) y cuándo no
panic existe, pero es para bugs del programador (índice fuera de rango, nil dereference) o estados irrecuperables al arrancar — no para "usuario no encontrado". recover() dentro de un defer puede atrapar un panic, y su único uso legítimo común es en middleware HTTP: que una petición rota no tumbe el servidor entero.
func middlewareRecover(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if p := recover(); p != nil {
log.Println("panic atrapado:", p)
http.Error(w, "error interno", http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}Regla de oro: si estás usando panic como si fuera throw, estás escribiendo Java con sintaxis Go. No lo hagas.
// sin miedo: no es C
Punteros (los amables)
En Java todo objeto ya viaja por referencia y no lo eliges. En Go tú decides: pasar una copia (valor) o la dirección (puntero). No hay aritmética de punteros, no hay memoria manual — el GC sigue trabajando para ti. Es solo el control, sin los peligros de C.
func main() {
x := 10
p := &x // p es *int: "la dirección donde vive x"
*p = 25 // desreferenciar: escribir en esa dirección
fmt.Println(x) // 25
// Sin puntero: la función recibe una COPIA (no pasa nada afuera)
doblarCopia(x)
fmt.Println(x) // 25 (sin cambios)
// Con puntero: la función modifica el original
doblarReal(&x)
fmt.Println(x) // 50
}
func doblarCopia(n int) { n *= 2 }
func doblarReal(n *int) { *n *= 2 }Cuándo usar puntero, en tres reglas:
- Necesitas mutar el argumento (o el receptor de un método).
- El struct es grande y copiarlo en cada llamada sería caro.
- Necesitas distinguir "sin valor": un
*time.Timepuede sernil; untime.Timeno.
El NullPointerException sigue existiendoSe llama nil pointer dereference y provoca un panic. La diferencia: en Go solo pueden ser nil los punteros, slices, maps, canales, funciones e interfaces — un string o un int jamás. La superficie de ataque del "null" se reduce muchísimo, pero no desaparece.
// las dos estructuras del 95% de tu código
Slices y maps
Go no trae 30 colecciones como java.util. Trae dos que cubren casi todo: el slice (tu ArrayList) y el map (tu HashMap). Los arrays de tamaño fijo existen pero casi nunca los usarás directamente.
// Un slice es una VISTA sobre un array subyacente: {puntero, len, cap}
frutas := []string{"mango", "kiwi"} // literal
nums := make([]int, 0, 10) // len 0, capacidad 10
frutas = append(frutas, "uva") // append DEVUELVE el slice (¡reasigna!)
fmt.Println(len(frutas), cap(frutas)) // 3 4
// Slicing como Python, pero sin índices negativos
todos := []int{0, 1, 2, 3, 4, 5}
fmt.Println(todos[1:4]) // [1 2 3]
fmt.Println(todos[:2], todos[3:]) // [0 1] [3 4 5]
// Recorrer: índice y valor
for i, f := range frutas {
fmt.Println(i, f)
}
// Desde Go 1.21 el paquete "slices" trae lo que extrañabas de Streams:
slices.Contains(frutas, "kiwi") // true
slices.Sort(todos)
idx, encontrado := slices.BinarySearch(todos, 3)El gotcha #1 de Go: slices que comparten memoriaUn sub-slice b := a[2:5] NO copia datos: apunta al mismo array. Escribir en b modifica a. Si necesitas una copia independiente: c := slices.Clone(a) (o copy(dst, src)). Entiende el y este bug nunca te tocará.
edades := map[string]int{"ana": 28, "luis": 34}
edades["carla"] = 25
delete(edades, "luis")
// El patrón "comma ok": tu containsKey + get en una sola operación
edad, ok := edades["pedro"]
if !ok {
fmt.Println("pedro no existe (edad vale el zero value: 0)")
}
// Iteración: el ORDEN ES ALEATORIO a propósito (para que nadie dependa de él)
for nombre, edad := range edades {
fmt.Println(nombre, edad)
}
// ¿HashSet? Un map con valores vacíos, idioma estándar:
vistos := make(map[string]struct{})
vistos["id-42"] = struct{}{}
_, existe := vistos["id-42"]Equivalencias de colecciones
| Java | Go | Comentario |
|---|---|---|
ArrayList<T> | []T | Crece solo con append; acceso O(1) |
HashMap<K,V> | map[K]V | K debe ser comparable (==); no es thread-safe |
HashSet<T> | map[T]struct{} | struct{} ocupa 0 bytes |
Streams API | for + paquetes slices/maps | Go prefiere bucles explícitos; la lib samber/lo da map/filter si los extrañas |
ConcurrentHashMap | sync.Map o map + sync.RWMutex | El mutex explícito es lo idiomático en la mayoría de casos |
// llegaron en 2022, se usan con moderación
Generics
Desde Go 1.18 existen los type parameters, con corchetes en lugar de ángulos: func F[T any](x T). La sintaxis te resultará familiar de Java, con una diferencia cultural: la comunidad Go los usa para utilidades y contenedores, no para arquitecturas genéricas de tres niveles.
// T puede ser cualquier cosa; U también. Como <T, U> en Java.
func Map[T, U any](entrada []T, f func(T) U) []U {
salida := make([]U, 0, len(entrada))
for _, v := range entrada {
salida = append(salida, f(v))
}
return salida
}
// Constraint: "comparable" permite usar == (para keys de map, búsquedas...)
func Contiene[T comparable](lista []T, buscado T) bool {
for _, v := range lista {
if v == buscado {
return true
}
}
return false
}
// Constraints propios: una interfaz con UNIÓN de tipos. La ~ significa
// "cualquier tipo cuyo tipo base sea este" (incluye tipos derivados).
type Numero interface {
~int | ~int64 | ~float64
}
func Sumar[T Numero](nums []T) T {
var total T
for _, n := range nums {
total += n
}
return total
}
func main() {
dobles := Map([]int{1, 2, 3}, func(n int) int { return n * 2 })
fmt.Println(dobles) // [2 4 6]
fmt.Println(Sumar([]float64{1.5, 2.5})) // 4
}Cultura GoAntes de escribir una función genérica pregúntate si una interfaz pequeña o un tipo concreto resuelven igual. El proverbio local: "un poco de duplicación es más barato que una mala abstracción". Si vienes de Java te sonará a herejía; dale unas semanas.
// nuevo · Go 1.23
Iteradores: range sobre funciones
Desde Go 1.23, range también acepta funciones iteradoras: secuencias perezosas que producen valores bajo demanda, sin materializar slices intermedios. Es lo más parecido a los generators de Python (yield) o a un Stream de Java que verás en Go.
import "iter"
// Un iterador es una función que recibe un callback "yield".
// Si yield devuelve false, el consumidor hizo break: paramos.
func Pares(hasta int) iter.Seq[int] {
return func(yield func(int) bool) {
for i := 0; i <= hasta; i += 2 {
if !yield(i) {
return
}
}
}
}
func main() {
for p := range Pares(1_000_000) {
if p > 8 {
break // el iterador se detiene aquí: nunca generó el millón
}
fmt.Println(p) // 0 2 4 6 8
}
// La stdlib ya los usa: maps.Keys, slices.Values, strings.Lines...
for clave := range maps.Keys(misDatos) {
fmt.Println(clave)
}
}Para tu cabeza de Pythoniter.Seq[T] ≈ un generator; iter.Seq2[K,V] ≈ un generator de tuplas (lo usa maps.All). La diferencia: aquí no hay corrutinas mágicas — es una función normal con un callback, y por eso el compilador puede optimizarla a un bucle plano.