Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

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
23
public 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
17
public 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:

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
18
public 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 II

El 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

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
17
package 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
10
func 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:

  1. Exportado/no exportado: NewPersona arranca con mayúscula, entonces quien importe el paquete persona puede llamarlo. Si arrancara con minúscula (newPersona), sería interno del paquete.

  2. 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
4
type 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:

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
7
func (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:

2.5Constructor de Estudiante

1
2
3
4
5
6
7
8
9
10
func 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

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
4
type 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
26
func 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 II

Algunas observaciones:

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

ConceptoJavaGo
ReutilizaciónHerencia (extends)Composición (incrustación)
Jerarquía de tiposSí, obligatoriaNo existe
PolimorfismoLigadura dinámica por herenciaInterfaces implícitas
Visibilidadprivate, protected, publicMayúscula = exportado, minúscula = no exportado
ConstructoresCon el mismo nombre de la claseFunción NewTipo() que devuelve (*Tipo, error)
ErroresExcepciones (try/catch)Valores de retorno (error)
“this” / “self”this implícitoReceptor 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:

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.

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.