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

structs.go
// 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 toca a. 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

embedding.go
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.

interfaces.go
// 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"})
}
i

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:

assertions.go
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.

errores.go
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.

recover.go
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.

punteros.go
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.Time puede ser nil; un time.Time no.
!

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.

slices.go
// 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á.

maps.go
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

JavaGoComentario
ArrayList<T>[]TCrece solo con append; acceso O(1)
HashMap<K,V>map[K]VK debe ser comparable (==); no es thread-safe
HashSet<T>map[T]struct{}struct{} ocupa 0 bytes
Streams APIfor + paquetes slices/mapsGo prefiere bucles explícitos; la lib samber/lo da map/filter si los extrañas
ConcurrentHashMapsync.Map o map + sync.RWMutexEl 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.

generics.go
// 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
}
i

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.

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