Clase 4: Programación orientada a objetos en Go
Objetivos
- Comprender los cuatro principios fundamentales de la programación orientada a objetos (abstracción, encapsulamiento, herencia y polimorfismo) y su aplicación en el lenguaje de programación Go.
- Escribir un programa en Go que utilice estructuras (structs) para definir un tipo de dato personalizado, implemente métodos para manipular ese tipo de dato y utilice interfaces para permitir la interoperabilidad con otros tipos de datos.
- Utilizar la composición para construir una estructura compleja a partir de estructuras más simples, aplica el encapsulamiento para restringir el acceso a ciertos campos y métodos, y utiliza el polimorfismo a través de interfaces para permitir que diferentes tipos implementen un conjunto común de métodos.
Contenido de la clase:
1. Conceptos básicos de programación orientada a objetos
La programación orientada a objetos (POO) es un paradigma de programación que se basa en la idea de estructurar el código en torno a objetos que representan entidades del mundo real.
- Encapsulación
Es un principio de la programación orientada a objetos que consiste en ocultar los detalles internos de un objeto y proporcionar una interfaz controlada para interactuar con él. Es decir, se busca limitar el acceso directo a los datos y métodos internos de un objeto y en su lugar, se utilizan métodos públicos para interactuar con él.
💡La encapsulación contiene toda la información importante de un objeto dentro del mismo y solo expone la información seleccionada al mundo exterior.
- Abstracción
Este concepto está muy relacionado con el anterior
Como la propia palabra indica, el principio de abstracción lo que implica es que la clase debe representar las características de la entidad hacia el mundo exterior, pero ocultando la complejidad que lleva. O sea, nos abstrae de la complejidad que haya dentro dándonos una serie de atributos y comportamientos que podemos usar sin preocuparnos de qué pasa por dentro cuando lo hagamos.
💡La abstracción está muy relacionada con la encapsulación, pero va un paso más allá pues no sólo controla el acceso a la información, sino también oculta la complejidad de los procesos que estemos implementando.
- Herencia
La herencia permite que se puedan definir nuevas clases basadas de unas ya existentes a fin de reutilizar el código, generando así una jerarquía de clases dentro de una aplicación. Si una clase deriva de otra, esta hereda sus atributos y métodos y puede añadir nuevos atributos, métodos o redefinir los heredados.
💡Permite crear nuevas clases basadas en clases existentes.
- Polimorfismo
Es la capacidad de un objeto para tomar diferentes formas o comportarse de diferentes maneras.
💡Permite que objetos de diferentes clases respondan de manera distinta a un mismo mensaje ó método, lo que facilita la flexibilidad del código.
¿Por qué la programación orientada a objetos?
Permite que el código sea reutilizable, organizado y sencillo de mantener. Sigue el principio de desarrollo de software DRY (Don’t Repeat Yoursealf), para no duplicar código y crear programas eficientes. Asimismo previene el acceso no deseado a los datos o la exposición de código propietario a través de la encapsulación y la abstracción.
¿Es Go un lenguaje de programación orientado a objetos?
Go es un lenguaje de programación que admite la programación orientada a objetos de manera limitada en comparación a otros lenguajes de programación como Java.
https://go.dev/doc/faq#Is_Go_an_object-oriented_language
https://blog.friendsofgo.tech/posts/es_go_un_lenguaje_orientado_a_objetos/
¿Cómo es la programación orientada a objetos en Go?
A diferencia de lenguajes explícitamente orientados a objetos como Java, en Go no existen las clases, ni los objetos, ni la herencia de clases.
Existen tipos de datos definidos por el usuario a los cuales se les puede incorporar comportamientos.
En una analogía con una clase, las propiedades pudieran ser los tipos de datos de una estructura, y los métodos las funciones o comportamientos asociados al tipo de dato.
2. Definición de estructuras (structs), métodos e interfaces
En Go, las clases no existen como un concepto independiente como lo hacen en otros lenguajes de programación orientados a objetos. Sin embargo, es posible lograr funcionalidad similar a las clases utilizando structs y métodos asociados a ellos.
Structs
Es un tipo de dato que permite agrupar varios valores de diferentes tipos en una sola estructura. Los valores se llaman campos y cada campo tiene un nombre y un tipo.
Son útiles para agrupar datos relacionados en una sola estructura de datos y proporcionar una manera organizada y eficiente de trabajar con ellos.
💡En resumen, los structs en Go son una herramienta muy importante para organizar y manejar datos de manera eficiente y legible en tu código, y son esenciales para crea aplicaciones complejas y escalables.package main import "fmt" // Struct global type Person struct { name string age int } func main() { // Instanciar un struct carlos := Person{name: "Carlos"} carlos.age = 26 fmt.Printf("%+v", carlos) }
Métodos
En Go, los métodos son funciones asociadas a un tipo de dato específico. Se definen y se declaran dentro del alcance de un tipo, lo que permite que ese tipo tenga comportamientos específicos asociados a él. Los métodos son una forma de extender la funcionalidad de los tipos de datos y proporcionan una forma de realizar operaciones o acciones relacionadas con ese tipo.
Los métodos en Go se definen utilizando una función con un receptor (receiver) asociado al tipo en el que se definen. El receptor es un parámetro especial que permite que el método sea invocado en una instancia particular del tipo. El receptor se coloca antes del nombre de la función y puede ser un valor o un puntero al tipo.
Hay dos tipos de receptores en Go:
- Receptor de valor (value receiver): El método actúa sobre una copia del valor del receptor. Se utiliza cuando no se necesita modificar el valor del receptor.
- Receptor de puntero (pointer receiver): El método actúa directamente sobre el valor del receptor a través de un puntero. Se utiliza cuando se desea modificar el valor del receptor.
1. Declaramos una struct Person
package main import "fmt" // Struct global type Person struct { name string age int }- Delcaración de métodos
// Receptor de puntero func (p *Person) setName(name string) { p.name = name } // Receptor de valor func (p Person) getName() string { return p.name } // Receptor de puntero func (p *Person) setAge(age int) { p.age = age } // Receptor de valor func (p Person) getAge() int { return p.age } func (p Person) toString() string { return fmt.Sprintf("*** Nombre: %s, Edad: %d ***", p.getName(), p.getAge()) }- Instanciamos Person y utilizamos los métodos
func main() { // Creación de una instancia del tipo Person carlos := Person{} // Llamada a los métodos del tipo Person carlos.setName("Carlos") carlos.setAge(26) fmt.Println("Mi nombre es:", carlos.getName()) fmt.Println("Mi edad es: ", carlos.getAge()) fmt.Println(carlos.toString()) }- Resultado final
package main import "fmt" type Person struct { name string age int } func (p *Person) setName(name string) { p.name = name } func (p *Person) setAge(age int) { p.age = age } func (p Person) getName() string { return p.name } func (p Person) getAge() int { return p.age } func (p Person) toString() string { return fmt.Sprintf("*** Nombre: %s, Edad: %d ***", p.getName(), p.getAge()) } func main() { carlos := Person{} carlos.setName("Carlos") carlos.setAge(26) fmt.Println("Mi nombre es:", carlos.getName()) fmt.Println("Mi edad es: ", carlos.getAge()) fmt.Println(carlos.toString()) }En Go, es posible definir métodos no solo para structs, si no también para tipos de datos básicos y personalizados, como enteros, cadenas de texto y otros tipos definidos por el usuario utilizando la palabra clave
type.Esto se logra mediante la definición de un tipo nuevo basado en un tipo existente y la declaración de métodos asociados a ese nuevo tipo. A este proceso se le conoce como “receptor con nombre” (named receiver) en Go.
package main import ( "fmt" "strings" ) // Definición de un tipo nuevo basado en un tipo existente (string) type MyString string // Método asociado al tipo MyString: cuenta el número de caracteres en la cadena func (m MyString) length() int { return len(m) } // Método asociado al tipo MyString: convierte la cadena en mayúsculas func (m MyString) toUpper() string { return strings.ToUpper(string(m)) } func main() { text := MyString("Hola, mundo!") // Llamada a los métodos asociados al tipo MyString length := text.length() fmt.Println("Número de caracteres:", length) textUpper := text.toUpper() fmt.Println("Cadena en mayúsculas:", textUpper) }package main import "fmt" // Definición de un tipo nuevo basado en int type MyInt int // Método asociado al tipo MyInt: verifica si el entero es par func (i MyInt) IsEven() bool { return i%2 == 0 } // Método asociado al tipo MyInt: duplica el valor del entero func (i MyInt) Double() MyInt { return i * 2 } func main() { number := MyInt(7) // Llamada a los métodos asociados al tipo MyInt fmt.Println("¿Es par?", number.IsEven()) double := number.Double() fmt.Println("Valor double:", double) }
Interfaces
Las interfaces en Go son un tipo de dato que se usa para representar el comportamiento de otros tipos. Una interfaz es como un plano técnico o un contrato que un objeto debe cumplir. Cuando se usan interfaces, el código base se vuelve más flexible y adaptable, porque se escribe código que no está vinculado a una implementación concreta. Por tanto, se puede extender la funcionalidad de un programa rápidamente.
A diferencia de las interfaces de otros lenguajes de programación, en Go son de forma implícita. Go no ofrece palabras clave para implementar una interfaz (e.g. implements).
- Declaración de una interfaz
Una interfaz en Go es como un plano técnico. Un tipo abstracto que solo incluye los métodos que un tipo concreto debe poseer o implementar.
package main type Shape interface { Perimeter() float64 Area() float64 }- Implementar una interfaz
Como se mencionó anteriormente, en Go no hay una palabra clave para implementar una interfaz. Un tipo cumple de forma implícita una interfaz en Go cuando tiene todos los métodos que necesita una interfaz.
Ahora se creará una struct Square que tenga los dos métodos de la interfaz Shape.
type Square struct { size float64 } func (s Square) Area() float64 { return s.size * s.size } func (s Square) Perimeter() float64 { return s.size * 4 } func main() { // Instanciamos Square var s Shape = Square{3} fmt.Printf("%T\n", s) fmt.Println("Area: ", s.Area()) fmt.Println("Perimeter:", s.Perimeter()) }type Circle struct { radius float64 } func (c Circle) Area() float64 { return math.Pi * c.radius * c.radius } func (c Circle) Perimeter() float64 { return 2 * math.Pi * c.radius } func main() { // Instanciamos Square var s Shape = Square{3} fmt.Printf("%T\n", s) fmt.Println("Area: ", s.Area()) fmt.Println("Perimeter:", s.Perimeter()) // Instanciamos Circle c := Circle{6} fmt.Printf("%T\n", c) fmt.Println("Area: ", c.Area()) fmt.Println("Perimeter:", c.Perimeter()) }Ahora se refactorizará la función
mainy se creará una función para imprimir el tipo del objeto que recibe, junto con su área y perímetro.func printInformation(s Shape) { // Tipo de dato fmt.Printf("%T\n", s) // Área fmt.Println("Area: ", s.Area()) // Perímetro fmt.Println("Perimeter:", s.Perimeter()) // Salto de línea fmt.Println() }func main() { var s Shape = Square{3} printInformation(s) c := Circle{6} printInformation(c) }
Constructores [Extra]
Go no tiene un mecanismo similar a los constructores de otros lenguajes orientados a objetos. Pero, es bastante común crear una función que los emule, devuelviendo un objeto ya inicializado con los parámetros que querramos aplicar.
package main import "fmt" type Person struct { name string age int active bool }- Crear un
structcon valoreszeropor defecto.
func main() { p := Person{} fmt.Println(p) }- Asignarle valores a las propiedades.
func main() { p := Person{name: "Carlos", age: 26, active: true} fmt.Println(p) }- Usando la palabra reservada
new: Regresa una referencia de memoria a la instancia creada, para acceder al valor usamos el operador*
func main() { p := new(Person) fmt.Println(p) }- Crear una función que actúe como método constructor: La función regresa con
¶ que regresa la referencia de memoria y no una copia de lastruct.
func NewPerson(name string, age int, active bool) Person { return Person{ name: name, age: age, active: active, } } func main() { p := NewPerson("Carlos", 26, true) fmt.Println(p) }- Crear un
3. Composición, encapsulamiento y polimorfismo en Go
Composición
En Go no existe la herencia, en su lugar, Go utiliza la composición como un enfoque para la reutilización de código y la construcción de relaciones entre tipos de datos
La composición es un concepto en la programación orientada a objetos que permite construir objetos complejos combinando o "componiendo" varios objetos más simples. En lugar de heredar directamente características de una clase base, la composición utiliza objetos como componentes para construir una clase más grande y más especializada.
En Go, la composición se logra mediante la inclusión de un tipo de estructura dentro de otra estructura como un campo. Esto permite que los campos y métodos del tipo incluido estén disponibles en el tipo que lo contiene.
package main import "fmt" // Definición de la estructura "Engine" type Engine struct { Type string Potencia int } // Definición de la estructura "Car" que utiliza composición para incluir un "Engine" type Car struct { Model string Color string Engine Engine // Campo "Engine" como parte de la composición } func main() { // Creación de un objeto "Engine" engine := Engine{Type: "Gasolina", Potencia: 150} // Creación de un objeto "Car" que incluye el objeto "Engine" car := Car{Model: "Sedán", Color: "Rojo", Engine: engine} // Acceso a los campos del objeto "Car" fmt.Println("Modelo:", car.Model) fmt.Println("Color:", car.Color) // Acceso a los campos y métodos del objeto "Engine" a través del objeto "Car" fmt.Println("Tipo de motor:", car.Engine.Type) fmt.Println("Potencia del motor:", car.Engine.Potencia) }
Encapsulamiento
El encapsulamiento permite proteger los datos internos de un objeto y garantizar que solo puedan ser modificados a través de métodos específicos. Esto ayuda a mantener la integridad de los datos y facilita el mantenimiento y la evolución del código.
En Go no existen identificadores de privacidad tales como public, protected y pirvate típicos de otros lenguajes de programación. Go encapsula tipos de datos y funciones a nivel de paquetes en base a convenciones de nombres.
Todos aquellos nombres que empiecen con mayúsculas serán accesibles (visibles) desde otros paquetes. Por el contrario aquellos que comiencen con minúsculas serán privados.
- Estructura del proyecto

- Archivo main.go
package main import ( . "curso-practico-go/poo/person" "fmt" ) func main() { // Creación de un objeto "Persona" persona := Persona{} // Establecer el nombre y la edad a través de los métodos de encapsulamiento persona.SetNombre("Carlos") persona.SetEdad(26) // Obtener el nombre y la edad a través de los métodos de encapsulamiento fmt.Println("Nombre:", persona.GetNombre()) fmt.Println("Edad:", persona.GetEdad()) }- Archivo person.go
package person // Definición de la estructura "Persona" type Persona struct { nombre string edad int } // Método para establecer el nombre de la persona func (p *Persona) SetNombre(nombre string) { p.nombre = nombre } // Método para obtener el nombre de la persona func (p Persona) GetNombre() string { return p.nombre } // Método para establecer la edad de la persona func (p *Persona) SetEdad(edad int) { p.edad = edad } // Método para obtener la edad de la persona func (p Persona) GetEdad() int { return p.edad }
Polimorfismo
En Go, el polimorfismo se logra mediante la implementación de interfaces. Una interfaz como ya vimos es una colección de métodos que se definen sin implementación. Una estructura o tipo de dato puede implementar una interfaz si proporciona una implementación para todos los métodos definidos en la interfaz.
- Declaración de interfaz
package main import "fmt" // Declaramos una interfaz llamada Animal type Animal interface { Sound() string }- Declaramos los structs Dog y Cat
// Declaramos el struct Dog type Dog struct {} // Declaramos el struct Cat type Cat struct {}- Definimos los métodos para los structs
// Método para el struct Dog func (d Dog) Sound() string { return "Woof!" } // Método para el struct Cat func (c Cat) Sound() string { return "Meow!" }- Utilizamos la interfaz creando una función
func PrintSound(a Animal) { fmt.Printf("%T \n", a) fmt.Println("El sonido es:", a.Sound()) }- Instanciamos los structs y utilizamos la función PrintSound()
func main() { // Instanciamos Cat myCat := Cat{} PrintSound(myCat) // Instanciamos Dog myDog := Dog{} PrintSound(myDog) }- Conclusión
El polimorfismo nos permite tratar estos objetos de diferentes tipos de manera uniforme a través de la interfaz común. En el caso de
PrintSound(), independientemente de si se le pasa unDogo unCatcomo argumento, puede llamar el métodoSound()en el parámetroa Animal. Esto se debe a queDogyCatimplementan la interfazAnimaly proporcionan una implementación del métodoSound(). La funciónPrintSound()no necesita conocer el tipo subayacente real del objeto que recibe, solo necesita asegurarse de que se implemente la interfazAnimal.El uso de interfaces y el polimorfismo nos permite escribir código más genérico y flexible, ya que nos permite tratar objetos de diferentes tipos de manera uniforme. A través de las interfaces, podemos definir un contrato común para múltiples tipos y escribir funciones que operen en esos tipos sin conocer su tipo subayacente real, lo que facilita la extensibilidad y reutilización del código.
4. Tarea
- Crear una struct "Rectangle" con métodos para calcular el área y el perímetro.
// Solución
package main
import "fmt"
type Rectangle struct {
Length float64
Width float64
}
func (r Rectangle) CalculateArea() float64 {
return r.Length * r.Width
}
func (r Rectangle) CalculatePerimeter() float64 {
return 2 * (r.Length + r.Width)
}
func main() {
rect := Rectangle{Length: 5, Width: 3}
area := rect.CalculateArea()
fmt.Println("El área del rectángulo es:", area)
perimeter := rect.CalculatePerimeter()
fmt.Println("El perímetro del rectángulo es:", perimeter)
}- Crear una struct "BankAccount" con métodos para depositar, retirar y consultar el saldo.
// Solución
package main
import "fmt"
type BankAccount struct {
Owner string
Balance float64
}
func (b *BankAccount) Deposit(amount float64) {
b.Balance += amount
}
func (b *BankAccount) Withdraw(amount float64) {
if amount <= b.Balance {
b.Balance -= amount
} else {
fmt.Println("Saldo insuficiente")
}
}
func (b BankAccount) CheckBalance() float64 {
return b.Balance
}
func main() {
account := BankAccount{Owner: "Carlos Córdova", Balance: 1000}
account.Deposit(500)
fmt.Println("Saldo después del depósito:", account.CheckBalance())
account.Withdraw(200)
fmt.Println("Saldo después del retiro:", account.CheckBalance())
}5. Recursos
- Uso de Structs: https://apuntes.de/golang/uso-de-estructuras/
- Uso de Methods: https://apuntes.de/golang/metodos/
- Uso de Interfaces: https://apuntes.de/golang/interfaces/
- Structs, Herencia, Polimorfismo y Encapsulación: https://coffeebytes.dev/go-structs-herencia-polimorfismo-y-encapsulacion/
- ¿Es Go un lenguaje orientado a objetos?: https://blog.friendsofgo.tech/posts/es_go_un_lenguaje_orientado_a_objetos/
- Herencia / Composición: https://daniel-m-spiridione.gitbook.io/designpatternsingo/parte1/poo/composicion
- Programación Orientada a Objetos en GO: https://jfvilladiego3.medium.com/programación-orientada-a-objetos-con-go-en-práctica-5694f69c2b56
- Methods and interfaces: https://go.dev/tour/methods/1
- Structs Go Simplified: https://youtu.be/cyGjLsTlQck