De FAQ: Is Go an object-oriented language?:
Sí y no. Aunque Go tiene tipos y métodos y permite un estilo de programación orientado a objetos, no existe una jerarquía de tipos. El concepto de “interfaz” en Go proporciona un enfoque diferente que creemos que es fácil de usar y, en cierto modo, más general. También hay formas de incrustar tipos en otros tipos para proporcionar algo análogo, pero no idéntico, a la creación de subclases. Además, los métodos en Go son más generales que en C++ o Java: se pueden definir para cualquier tipo de datos, incluso tipos integrados, como enteros “sin caja”. No están restringidos a estructuras (clases).
Además, la falta de una jerarquía de tipos hace que los “objetos” en Go parezcan mucho más ligeros que en lenguajes como C++ o Java.
En este capítulo vamos a comparar cómo se modelan objetos con estado y comportamiento en Java frente a Go. Para eso vamos a usar un ejemplo concreto: una Persona con datos básicos y un Estudiante que extiende a Persona agregando información propia.
1Java: herencia de clases¶
En Java, la relación entre Persona y Estudiante se modela con herencia: Estudiante extends Persona. Empecemos por Persona:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23public class Persona { private String nombre; private String direccion; private String dni; public Persona(String nombre, String direccion, String dni) { this.nombre = nombre; this.direccion = direccion; this.dni = dni; } public String saludar() { return "¡Hola! Soy " + nombre; } public String caminar() { return nombre + " está caminando"; } public String getNombre() { return nombre; } }
Ahora Estudiante hereda de Persona y agrega su propio atributo y método:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17public class Estudiante extends Persona { private int legajo; public Estudiante(String nombre, String direccion, String dni, int legajo) { super(nombre, direccion, dni); this.legajo = legajo; } @Override public String saludar() { return "¡Hola! Soy " + getNombre() + " y soy estudiante (legajo: " + legajo + ")"; } public String cursarMateria(String materia) { return getNombre() + " se inscribió en " + materia; } }
Notar que:
Usamos
super()para llamar al constructor dePersona.Sobrescribimos (
@Override) el métodosaludar()para personalizarlo.caminar()no se sobrescribe, así queEstudianteusa la versión dePersona.Como los atributos de
Personasonprivate, accedemos anombremediante el gettergetNombre().
1.1Polimorfismo en Java¶
Un main que aprovecha el polimorfismo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18public class Main { public static void main(String[] args) { Persona[] personas = new Persona[2]; personas[0] = new Persona("Marcelo", "Av. Siempre Viva 123", "12345678"); personas[1] = new Estudiante("Laura", "Calle Falsa 456", "87654321", 2025001); for (Persona p : personas) { System.out.println(p.saludar()); System.out.println(p.caminar()); if (p instanceof Estudiante) { System.out.println(((Estudiante) p).cursarMateria("Algoritmos II")); } System.out.println(); } } }
¡Hola! Soy Marcelo
Marcelo está caminando
¡Hola! Soy Laura y soy estudiante (legajo: 2025001)
Laura está caminando
Laura se inscribió en Algoritmos IIEl array Persona[] contiene tanto Persona como Estudiante. Al iterar, Java resuelve qué versión de saludar() ejecutar según el tipo real del objeto (ligadura dinámica). A cursarMateria() solo lo podemos llamar si preguntamos con instanceof y casteamos.
Diagrama de clases Java: herencia
Diagrama de clases Java: herencia
2Go: composición con structs¶
Go no tiene herencia. En su lugar, usa composición: se arma un tipo nuevo incrustando otro tipo existente. Veamos paso a paso cómo modelar el mismo ejemplo.
2.1Persona¶
Primero definimos la estructura y sus métodos:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17package persona import "fmt" type Persona struct { nombre string direccion string dni string } func (p Persona) Saludar() string { return fmt.Sprintf("¡Hola! Soy %s", p.nombre) } func (p Persona) Caminar() string { return fmt.Sprintf("%s está caminando", p.nombre) }
Los campos nombre, direccion y dni empiezan con minúscula. En Go esto significa que son no exportados: solo se puede acceder a ellos desde el mismo paquete. Los métodos Saludar y Caminar arrancan con mayúscula, así que son exportados (visibles desde afuera del paquete).
2.2Constructor con validación¶
Go no tiene constructores como Java. Por convención, se usa una función NewPersona que devuelve el puntero y, si algo sale mal, un error:
1 2 3 4 5 6 7 8 9 10func NewPersona(nombre, direccion, dni string) (*Persona, error) { if dni == "" { return nil, fmt.Errorf("el DNI no puede estar vacío") } return &Persona{ nombre: nombre, direccion: direccion, dni: dni, }, nil }
Acá aparecen dos conceptos importantes:
Exportado/no exportado:
NewPersonaarranca con mayúscula, entonces quien importe el paquetepersonapuede llamarlo. Si arrancara con minúscula (newPersona), sería interno del paquete.Error como valor: en lugar de lanzar una excepción (como en Java), devolvemos un
error. Quien llama decide qué hacer con él.
2.3Estudiante con composición¶
Para modelar Estudiante, en lugar de heredar componemos Estudiante a partir de Persona, la struct Estudiante contiene una Persona como campo embebido (embedded) y le agrega sus propios campos y métodos. Persona no tiene un nombre de campo definido.
1 2 3 4type Estudiante struct { Persona legajo int }
Al escribir Persona sin nombre de campo, decimos que Estudiante está compuesto por una Persona a la que se le agregan atributos y comportamiento. Esto significa que:
Los campos de
Persona(nombre,direccion,dni) se promueven aEstudiante: comoEstudiantecontiene internamente unaPersona, puede acceder a sus campos directamente comoe.nombre,e.direccion, etc.Los métodos de
Persona(Saludar,Caminar) también se promueven: están disponibles enEstudiantesin necesidad de redefinirlos, a menos que queramos escribir una versión distinta.Como
PersonayEstudianteestán en el mismo paquete, los campos no exportados dePersonason accesibles desdeEstudiante.
2.4Override de métodos y nuevos métodos¶
Podemos definir un método con el mismo nombre para que haga algo distinto:
1 2 3 4 5 6 7func (e Estudiante) Saludar() string { return fmt.Sprintf("¡Hola! Soy %s y soy estudiante (legajo: %d)", e.nombre, e.legajo) } func (e Estudiante) CursarMateria(materia string) string { return fmt.Sprintf("%s se inscribió en %s", e.nombre, materia) }
Notar que:
Accedemos a
e.nombredirectamente (está promovido desdePersona).Caminar()no se redefine, así queEstudianteusa la versión dePersona.El constructor
NewEstudiantellama aNewPersonainternamente y agrega su propia validación.
2.5Constructor de Estudiante¶
1 2 3 4 5 6 7 8 9 10func NewEstudiante(nombre, direccion, dni string, legajo int) (*Estudiante, error) { if legajo <= 0 { return nil, fmt.Errorf("el legajo debe ser positivo, se recibió: %d", legajo) } p, err := NewPersona(nombre, direccion, dni) if err != nil { return nil, err } return &Estudiante{Persona: *p, legajo: legajo}, nil }
Si el legajo es inválido, retornamos error sin crear el Estudiante. Si la validación de NewPersona falla (por ejemplo DNI vacío), propagamos ese error.
Diagrama de structs Go: composición
Diagrama de structs Go: composición
La relación entre Estudiante y Persona no es herencia sino composición: Estudiante está compuesto por una Persona más el campo legajo. El rombo relleno en el diagrama indica esto.
3Polimorfismo en Go con interfaces¶
En Java el polimorfismo viene dado por la herencia: un Estudiante es una Persona. En Go el polimorfismo se logra con interfaces. Definimos una interfaz que describa el comportamiento común:
1 2 3 4type PersonaInterface interface { Saludar() string Caminar() string }
Tanto *Persona como *Estudiante implementan esta interfaz automáticamente (porque ambos tienen Saludar() string y Caminar() string). No hace falta declarar implements.
Ahora podemos escribir un main que itere sobre un slice de PersonaInterface:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26func main() { marcelo, err := persona.NewPersona("Marcelo", "Av. Siempre Viva 123", "12345678") if err != nil { fmt.Println(err) return } laura, err := persona.NewEstudiante("Laura", "Calle Falsa 456", "87654321", 2025001) if err != nil { fmt.Println(err) return } personas := []persona.PersonaInterface{marcelo, laura} for _, p := range personas { fmt.Println(p.Saludar()) fmt.Println(p.Caminar()) if e, ok := p.(*persona.Estudiante); ok { fmt.Println(e.CursarMateria("Algoritmos II")) } fmt.Println() } }
¡Hola! Soy Marcelo
Marcelo está caminando
¡Hola! Soy Laura y soy estudiante (legajo: 2025001)
Laura está caminando
Laura se inscribió en Algoritmos IIAlgunas observaciones:
El comportamiento es idéntico al ejemplo de Java, pero el mecanismo es distinto.
En Java,
Persona[]funciona porqueEstudiantees unaPersonapor herencia.En Go,
[]PersonaInterfacefunciona porque tanto*Personacomo*Estudianteimplementan la interfaz.Para llamar a
CursarMateriahacemos una type assertion (p.(*persona.Estudiante)) y verificamos conok. Es análogo alinstanceofy casteo de Java. Esta limitación existe porque, al no haber herencia, no podemos tratar aEstudiantecomo unaPersonaen tiempo de compilación. La interfaz permite polimorfismo sobre los métodos comunes, pero los métodos específicos deEstudianterequieren preguntar por el tipo concreto.
Una ventaja que Go tiene sobre Java es que un tipo puede implementar varias interfaces a la vez, lo que permite simular la herencia múltiple (un tipo que tenga comportamiento de varias fuentes distintas). En Java las clases solo pueden heredar de una, aunque pueden implementar varias interfaces. En Go no hay herencia, pero cualquier struct puede implementar cualquier cantidad de interfaces con solo definir los métodos correspondientes.
4Comparación: herencia vs composición¶
| Concepto | Java | Go |
|---|---|---|
| Reutilización | Herencia (extends) | Composición (incrustación) |
| Jerarquía de tipos | Sí, obligatoria | No existe |
| Polimorfismo | Ligadura dinámica por herencia | Interfaces implícitas |
| Visibilidad | private, protected, public | Mayúscula = exportado, minúscula = no exportado |
| Constructores | Con el mismo nombre de la clase | Función NewTipo() que devuelve (*Tipo, error) |
| Errores | Excepciones (try/catch) | Valores de retorno (error) |
| “this” / “self” | this implícito | Receptor explícito (p Persona) |
5Conclusión¶
Go no es un lenguaje orientado a objetos en el sentido clásico. No tiene clases, ni herencia, ni @Override. Pero ofrece las herramientas necesarias para modelar objetos:
Structs para agrupar datos.
Métodos con receptores para asociar comportamiento.
Incrustación para composición y reutilización.
Interfaces implícitas para polimorfismo.
El resultado es un estilo más liviano, donde la jerarquía de tipos no domina el diseño. En vez de preguntar “¿qué clase es esto?”, en Go preguntamos “¿qué comportamientos tiene?”.
6Ejercicios¶
Diagrama de structs del ejercicio de figuras geométricas.
Diagrama de structs del ejercicio de figuras geométricas.
Los ejercicios de este capítulo están en 11-oop/ejercicios/sistema-figuras/
del repositorio taller-go.
El directorio contiene un README.md con el enunciado y los esqueletos
para resolverlo.